Skip to content

Commit

Permalink
chore: refactor model hook support to live in the model package (#8499)
Browse files Browse the repository at this point in the history
* chore: extract model/json:api hooks installation into meta package

* cleanup

* type cleanup, polish model refactor

* more cleanup

* fix lint

* remove legacy-compat from store

* eliminate more peer-deps

* fix lint

* fix tests

* fix more tests

* dereference graph after destroy

* fix store teardown
  • Loading branch information
runspired authored Jul 14, 2023
1 parent 98872e9 commit e9e54e4
Show file tree
Hide file tree
Showing 30 changed files with 417 additions and 368 deletions.
20 changes: 20 additions & 0 deletions ember-data-types/q/ds-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ import type EmberObject from '@ember/object';
import type { Errors } from '@ember-data/model/-private';
import type Store from '@ember-data/store';

import { Cache } from './cache';
import { StableRecordIdentifier } from './identifier';
import type { JsonApiError } from './record-data-json-api';
import type { AttributeSchema, RelationshipSchema, RelationshipsSchema } from './record-data-schemas';

export type ModelFactory = { class: DSModelSchema };
export type FactoryCache = Record<string, ModelFactory>;
// we put this on the store for interop because it's used by modelFor and
// instantiateRecord as well.
export type ModelStore = Store & { _modelFactoryCache: FactoryCache };

// Placeholder until model.js is typed
export interface DSModel extends EmberObject {
constructor: DSModelSchema;
Expand Down Expand Up @@ -37,11 +45,23 @@ export interface ModelSchema {
toString(): string;
}

export type DSModelCreateArgs = {
_createProps: Record<string, unknown>;
// TODO @deprecate consider deprecating accessing record properties during init which the below is necessary for
_secretInit: {
identifier: StableRecordIdentifier;
cache: Cache;
store: Store;
cb: (record: DSModel, cache: Cache, identifier: StableRecordIdentifier, store: Store) => void;
};
};

// This is the static side of DSModel should become DSModel
// once we can type it.
export interface DSModelSchema extends ModelSchema {
isModel: true;
relationshipsObject: RelationshipsSchema;
extend(...mixins: unknown[]): DSModelSchema;
reopenClass(...mixins: unknown[]): void;
create(createArgs: DSModelCreateArgs): DSModel;
}
43 changes: 42 additions & 1 deletion packages/-ember-data/addon/-private/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,58 @@ import ArrayProxy from '@ember/array/proxy';
import PromiseProxyMixin from '@ember/object/promise-proxy-mixin';
import ObjectProxy from '@ember/object/proxy';

import { graphFor } from '@ember-data/graph/-private';
import JSONAPICache from '@ember-data/json-api';
import { LegacyNetworkHandler } from '@ember-data/legacy-compat';
import { FetchManager } from '@ember-data/legacy-compat/-private';
import { buildSchema, instantiateRecord, modelFor, teardownRecord } from '@ember-data/model/hooks';
import RequestManager from '@ember-data/request';
import Fetch from '@ember-data/request/fetch';
import BaseStore, { CacheHandler } from '@ember-data/store';
import BaseStore, { CacheHandler, recordIdentifierFor } from '@ember-data/store';
import type { Cache } from '@ember-data/types/cache/cache';
import type { CacheStoreWrapper } from '@ember-data/types/q/cache-store-wrapper';
import type { DSModel, ModelSchema, ModelStore } from '@ember-data/types/q/ds-model';
import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
import type { RecordInstance } from '@ember-data/types/q/record-instance';

export class Store extends BaseStore {
constructor(args: Record<string, unknown>) {
super(args);
this.requestManager = new RequestManager();
this.requestManager.use([LegacyNetworkHandler, Fetch]);
this.requestManager.useCache(CacheHandler);
this.registerSchema(buildSchema(this));
graphFor(this);
}

createCache(storeWrapper: CacheStoreWrapper): Cache {
return new JSONAPICache(storeWrapper);
}

instantiateRecord(
this: ModelStore,
identifier: StableRecordIdentifier,
createRecordArgs: Record<string, unknown>
): DSModel {
return instantiateRecord.call(this, identifier, createRecordArgs);
}

teardownRecord(record: RecordInstance): void {
teardownRecord.call(this, record as DSModel);
}

modelFor(type: string): ModelSchema {
return modelFor.call(this, type) || super.modelFor(type);
}

// TODO @runspired @deprecate records should implement their own serialization if desired
serializeRecord(record: RecordInstance, options?: Record<string, unknown>): unknown {
// TODO we used to check if the record was destroyed here
if (!this._fetchManager) {
this._fetchManager = new FetchManager(this);
}

return this._fetchManager.createSnapshot(recordIdentifierFor(record)).serialize(options);
}
}

Expand Down
7 changes: 3 additions & 4 deletions packages/adapter/src/-private/utils/serialize-into-hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import { assert } from '@ember/debug';
import type { Snapshot } from 'ember-data/-private';

import type Store from '@ember-data/store';
import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class';
import type { DSModelSchema } from '@ember-data/types/q/ds-model';
import type { ModelSchema } from '@ember-data/types/q/ds-model';
import type { MinimumSerializerInterface } from '@ember-data/types/q/minimum-serializer-interface';

type SerializerWithSerializeIntoHash = MinimumSerializerInterface & {
serializeIntoHash?(hash: {}, modelClass: ShimModelClass, snapshot: Snapshot, options?: { includeId?: boolean }): void;
serializeIntoHash?(hash: {}, modelClass: ModelSchema, snapshot: Snapshot, options?: { includeId?: boolean }): void;
};

export default function serializeIntoHash(
store: Store,
modelClass: ShimModelClass | DSModelSchema,
modelClass: ModelSchema,
snapshot: Snapshot,
options: { includeId?: boolean } = { includeId: true }
) {
Expand Down
16 changes: 8 additions & 8 deletions packages/adapter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ import { inject as service } from '@ember/service';
import { DEBUG } from '@ember-data/env';
import type { Snapshot, SnapshotRecordArray } from '@ember-data/legacy-compat/-private';
import type Store from '@ember-data/store';
import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class';
import type { ModelSchema } from '@ember-data/types/q/ds-model';
import type { AdapterPayload, MinimumAdapterInterface } from '@ember-data/types/q/minimum-adapter-interface';

/**
Expand Down Expand Up @@ -293,7 +293,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf
@public
*/
// @ts-expect-error
findRecord(store: Store, type: ShimModelClass, id: string, snapshot: Snapshot): Promise<AdapterPayload> {
findRecord(store: Store, type: ModelSchema, id: string, snapshot: Snapshot): Promise<AdapterPayload> {
if (DEBUG) {
throw new Error('You subclassed the Adapter class but missing a findRecord override');
}
Expand Down Expand Up @@ -332,7 +332,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf
*/
findAll(
store: Store,
type: ShimModelClass,
type: ModelSchema,
neverSet,
snapshotRecordArray: SnapshotRecordArray
// @ts-expect-error
Expand Down Expand Up @@ -375,7 +375,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf
@public
*/
// @ts-expect-error
query(store: Store, type: ShimModelClass, query): Promise<AdapterPayload> {
query(store: Store, type: ModelSchema, query): Promise<AdapterPayload> {
if (DEBUG) {
throw new Error('You subclassed the Adapter class but missing a query override');
}
Expand Down Expand Up @@ -420,7 +420,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf
@public
*/
// @ts-expect-error
queryRecord(store: Store, type: ShimModelClass, query, adapterOptions): Promise<AdapterPayload> {
queryRecord(store: Store, type: ModelSchema, query, adapterOptions): Promise<AdapterPayload> {
if (DEBUG) {
throw new Error('You subclassed the Adapter class but missing a queryRecord override');
}
Expand Down Expand Up @@ -530,7 +530,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf
@public
*/
// @ts-expect-error
createRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise<AdapterPayload> {
createRecord(store: Store, type: ModelSchema, snapshot: Snapshot): Promise<AdapterPayload> {
if (DEBUG) {
throw new Error('You subclassed the Adapter class but missing a createRecord override');
}
Expand Down Expand Up @@ -587,7 +587,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf
@public
*/
// @ts-expect-error
updateRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise<AdapterPayload> {
updateRecord(store: Store, type: ModelSchema, snapshot: Snapshot): Promise<AdapterPayload> {
if (DEBUG) {
throw new Error('You subclassed the Adapter class but missing a updateRecord override');
}
Expand Down Expand Up @@ -636,7 +636,7 @@ export default class Adapter extends EmberObject implements MinimumAdapterInterf
@public
*/
// @ts-expect-error
deleteRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise<AdapterPayload> {
deleteRecord(store: Store, type: ModelSchema, snapshot: Snapshot): Promise<AdapterPayload> {
if (DEBUG) {
throw new Error('You subclassed the Adapter class but missing a deleteRecord override');
}
Expand Down
6 changes: 3 additions & 3 deletions packages/adapter/src/json-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { pluralize } from 'ember-inflector';

import type { Snapshot } from '@ember-data/legacy-compat/-private';
import type Store from '@ember-data/store';
import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class';
import type { ModelSchema } from '@ember-data/types/q/ds-model';
import type { AdapterPayload } from '@ember-data/types/q/minimum-adapter-interface';

import { serializeIntoHash } from './-private';
Expand Down Expand Up @@ -251,7 +251,7 @@ class JSONAPIAdapter extends RESTAdapter {
this._coalesceFindRequests = value;
}

findMany(store: Store, type: ShimModelClass, ids: string[], snapshots: Snapshot[]): Promise<AdapterPayload> {
findMany(store: Store, type: ModelSchema, ids: string[], snapshots: Snapshot[]): Promise<AdapterPayload> {
let url = this.buildURL(type.modelName, ids, snapshots, 'findMany');
return this.ajax(url, 'GET', { data: { filter: { id: ids.join(',') } } });
}
Expand All @@ -261,7 +261,7 @@ class JSONAPIAdapter extends RESTAdapter {
return pluralize(dasherized);
}

updateRecord(store: Store, schema: ShimModelClass, snapshot: Snapshot): Promise<AdapterPayload> {
updateRecord(store: Store, schema: ModelSchema, snapshot: Snapshot): Promise<AdapterPayload> {
const data = serializeIntoHash(store, schema, snapshot);
const type = snapshot.modelName;
const id = snapshot.id;
Expand Down
18 changes: 9 additions & 9 deletions packages/adapter/src/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { join } from '@ember/runloop';
import { DEBUG } from '@ember-data/env';
import type { Snapshot, SnapshotRecordArray } from '@ember-data/legacy-compat/-private';
import type Store from '@ember-data/store';
import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class';
import type { ModelSchema } from '@ember-data/types/q/ds-model';
import type { AdapterPayload } from '@ember-data/types/q/minimum-adapter-interface';

import { determineBodyPromise, fetch, parseResponseHeaders, serializeIntoHash, serializeQueryParams } from './-private';
Expand Down Expand Up @@ -566,7 +566,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) {
@param {Snapshot} snapshot
@return {Promise} promise
*/
findRecord(store: Store, type: ShimModelClass, id: string, snapshot: Snapshot): Promise<AdapterPayload> {
findRecord(store: Store, type: ModelSchema, id: string, snapshot: Snapshot): Promise<AdapterPayload> {
let url = this.buildURL(type.modelName, id, snapshot, 'findRecord');
let query: QueryState = this.buildQuery(snapshot);

Expand All @@ -590,7 +590,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) {
*/
findAll(
store: Store,
type: ShimModelClass,
type: ModelSchema,
sinceToken,
snapshotRecordArray: SnapshotRecordArray
): Promise<AdapterPayload> {
Expand Down Expand Up @@ -624,7 +624,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) {
@param {Object} adapterOptions
@return {Promise} promise
*/
query(store: Store, type: ShimModelClass, query): Promise<AdapterPayload> {
query(store: Store, type: ModelSchema, query): Promise<AdapterPayload> {
let url = this.buildURL(type.modelName, null, null, 'query', query);

if (this.sortQueryParams) {
Expand Down Expand Up @@ -656,7 +656,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) {
*/
queryRecord(
store: Store,
type: ShimModelClass,
type: ModelSchema,
query: Record<string, unknown>,
adapterOptions: Record<string, unknown>
): Promise<AdapterPayload> {
Expand Down Expand Up @@ -703,7 +703,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) {
@param {Array} snapshots
@return {Promise} promise
*/
findMany(store: Store, type: ShimModelClass, ids: string[], snapshots: Snapshot[]): Promise<AdapterPayload> {
findMany(store: Store, type: ModelSchema, ids: string[], snapshots: Snapshot[]): Promise<AdapterPayload> {
let url = this.buildURL(type.modelName, ids, snapshots, 'findMany');
return this.ajax(url, 'GET', { data: { ids: ids } });
}
Expand Down Expand Up @@ -829,7 +829,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) {
@param {Snapshot} snapshot
@return {Promise} promise
*/
createRecord(store: Store, type: ShimModelClass, snapshot: Snapshot): Promise<AdapterPayload> {
createRecord(store: Store, type: ModelSchema, snapshot: Snapshot): Promise<AdapterPayload> {
let url = this.buildURL(type.modelName, null, snapshot, 'createRecord');

const data = serializeIntoHash(store, type, snapshot);
Expand All @@ -854,7 +854,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) {
@param {Snapshot} snapshot
@return {Promise} promise
*/
updateRecord(store: Store, schema: ShimModelClass, snapshot: Snapshot): Promise<AdapterPayload> {
updateRecord(store: Store, schema: ModelSchema, snapshot: Snapshot): Promise<AdapterPayload> {
const data = serializeIntoHash(store, schema, snapshot, {});
const type = snapshot.modelName;
const id = snapshot.id;
Expand All @@ -876,7 +876,7 @@ class RESTAdapter extends Adapter.extend(BuildURLMixin) {
@param {Snapshot} snapshot
@return {Promise} promise
*/
deleteRecord(store: Store, schema: ShimModelClass, snapshot: Snapshot): Promise<AdapterPayload> {
deleteRecord(store: Store, schema: ModelSchema, snapshot: Snapshot): Promise<AdapterPayload> {
const type = snapshot.modelName;
const id = snapshot.id;
assert(`Attempted to delete the ${type} record, but the record has no id`, typeof id === 'string' && id.length > 0);
Expand Down
3 changes: 1 addition & 2 deletions packages/graph/src/-private/graph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,12 @@ export function graphFor(store: CacheStoreWrapper | Store): Graph {
if (!graph) {
graph = new Graph(wrapper);
Graphs.set(wrapper, graph);
getStore(wrapper)._graph = graph;

// in DEBUG we attach the graph to the main store for improved debuggability
if (DEBUG) {
if (getStore(wrapper).isDestroying) {
throw new Error(`Memory Leak Detected During Teardown`);
}
Graphs.set(getStore(wrapper) as unknown as CacheStoreWrapper, graph);
}
}
return graph;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import type Store from '@ember-data/store';
import { coerceId } from '@ember-data/store/-private';
import { StoreRequestInfo } from '@ember-data/store/-private/cache-handler';
import type { InstanceCache } from '@ember-data/store/-private/caches/instance-cache';
import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class';
import type RequestStateService from '@ember-data/store/-private/network/request-cache';
import type { ModelSchema } from '@ember-data/types/q/ds-model';
import type { CollectionResourceDocument, SingleResourceDocument } from '@ember-data/types/q/ember-data-json-api';
import type { FindRecordQuery, Request, SaveRecordMutation } from '@ember-data/types/q/fetch-manager';
import type {
Expand All @@ -30,7 +30,7 @@ import Snapshot from './snapshot';

type AdapterErrors = Error & { errors?: string[]; isAdapterError?: true };
type SerializerWithParseErrors = MinimumSerializerInterface & {
extractErrors?(store: Store, modelClass: ShimModelClass, error: AdapterErrors, recordId: string | null): unknown;
extractErrors?(store: Store, modelClass: ModelSchema, error: AdapterErrors, recordId: string | null): unknown;
};

export const SaveOp: unique symbol = Symbol('SaveOp');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { DEBUG, TESTING } from '@ember-data/env';
import type { Handler, NextFn } from '@ember-data/request/-private/types';
import type Store from '@ember-data/store';
import type { StoreRequestContext, StoreRequestInfo } from '@ember-data/store/-private/cache-handler';
import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class';
import type { Collection } from '@ember-data/store/-private/record-arrays/identifier-array';
import { SingleResourceDataDocument } from '@ember-data/types/cache/document';
import type { ModelSchema } from '@ember-data/types/q/ds-model';
import type {
CollectionResourceDocument,
JsonApiDocument,
Expand All @@ -33,7 +33,7 @@ import SnapshotRecordArray from './snapshot-record-array';

type AdapterErrors = Error & { errors?: unknown[]; isAdapterError?: true; code?: string };
type SerializerWithParseErrors = MinimumSerializerInterface & {
extractErrors?(store: Store, modelClass: ShimModelClass, error: AdapterErrors, recordId: string | null): unknown;
extractErrors?(store: Store, modelClass: ModelSchema, error: AdapterErrors, recordId: string | null): unknown;
};

const PotentialLegacyOperations = new Set([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { assert } from '@ember/debug';

import { DEBUG } from '@ember-data/env';
import type Store from '@ember-data/store';
import type ShimModelClass from '@ember-data/store/-private/legacy-model-support/shim-model-class';
import type { ModelSchema } from '@ember-data/types/q/ds-model';
import type { JsonApiDocument } from '@ember-data/types/q/ember-data-json-api';
import type { AdapterPayload } from '@ember-data/types/q/minimum-adapter-interface';
import type { MinimumSerializerInterface, RequestType } from '@ember-data/types/q/minimum-serializer-interface';
Expand Down Expand Up @@ -70,7 +70,7 @@ function validateDocumentStructure(doc?: AdapterPayload | JsonApiDocument): asse
export function normalizeResponseHelper(
serializer: MinimumSerializerInterface | null,
store: Store,
modelClass: ShimModelClass,
modelClass: ModelSchema,
payload: AdapterPayload,
id: string | null,
requestType: RequestType
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 @@ -41,7 +41,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', 'error.js', 'json-api.js', 'rest.js', '-private.js']),
addon.publicEntrypoints(['index.js', '-private.js', 'hooks.js']),

nodeResolve({ extensions: ['.ts', '.js'] }),
babel({
Expand Down
1 change: 0 additions & 1 deletion packages/model/src/-private.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export { default as Errors } from './-private/errors';
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';
export { default as _modelForMixin } from './-private/model-for-mixin';

// // Used by tests
export { LEGACY_SUPPORT } from './-private/model';
Loading

0 comments on commit e9e54e4

Please sign in to comment.