Ember Data fixes (#34245)

* ember-data: fix bugs and improve type safety.

* ember-data: handle `each(Transformed)?Attribute`.

* ember-data: fix a lint error.
This commit is contained in:
Chris Krycho 2019-03-29 12:19:02 -06:00 committed by Ron Buckton
parent 7e92d8b69d
commit a819975d58
5 changed files with 131 additions and 45 deletions

View File

@ -16,8 +16,14 @@ import ModelRegistry from 'ember-data/types/registries/model';
import SerializerRegistry from 'ember-data/types/registries/serializer';
import AdapterRegistry from 'ember-data/types/registries/adapter';
type AttributesFor<Model> = keyof Model; // TODO: filter to attr properties only (TS 2.8)
type RelationshipsFor<Model> = keyof Model; // TODO: filter to hasMany/belongsTo properties only (TS 2.8)
/**
The keys from the actual Model class, removing all the keys which come from
the base class.
*/
type ModelKeys<Model extends DS.Model> = Exclude<keyof Model, keyof DS.Model>;
type AttributesFor<Model extends DS.Model> = ModelKeys<Model>; // TODO: filter to attr properties only (TS 2.8)
type RelationshipsFor<Model extends DS.Model> = ModelKeys<Model>; // TODO: filter to hasMany/belongsTo properties only (TS 2.8)
export interface ChangedAttributes {
[key: string]: [any, any] | undefined;
@ -49,9 +55,9 @@ export namespace DS {
*/
function errorsArrayToHash(errors: any[]): {};
interface RelationshipOptions<Model> {
interface RelationshipOptions<M extends Model> {
async?: boolean;
inverse?: RelationshipsFor<Model> | null;
inverse?: RelationshipsFor<M> | null;
polymorphic?: boolean;
}
@ -457,12 +463,12 @@ export namespace DS {
* Create a JSON representation of the record, using the serialization
* strategy of the store's adapter.
*/
serialize(options?: { includeId?: boolean }): {};
serialize(options?: { includeId?: boolean }): object;
/**
* Use [DS.JSONSerializer](DS.JSONSerializer.html) to
* get the JSON representation of a record.
*/
toJSON(options: {}): {};
toJSON(options?: { includeId?: boolean }): object;
/**
* Fired when the record is ready to be interacted with,
* that is either loaded from the server or created locally.
@ -502,15 +508,15 @@ export namespace DS {
* method if you want to allow the user to still `rollbackAttributes()`
* after a delete was made.
*/
deleteRecord(): any;
deleteRecord(): void;
/**
* Same as `deleteRecord`, but saves the record immediately.
*/
destroyRecord(options?: {}): RSVP.Promise<any>;
destroyRecord(options?: { adapterOptions?: object }): RSVP.Promise<this>;
/**
* Unloads the record from the store. This will cause the record to be destroyed and freed up for garbage collection.
*/
unloadRecord(): any;
unloadRecord(): void;
/**
* Returns an object, whose keys are changed properties, and value is
* an [oldProp, newProp] array.
@ -520,16 +526,16 @@ export namespace DS {
* If the model `hasDirtyAttributes` this function will discard any unsaved
* changes. If the model `isNew` it will be removed from the store.
*/
rollbackAttributes(): any;
rollbackAttributes(): void;
/**
* Save the record and persist any changes to the record to an
* external source via the adapter.
*/
save(options?: {}): RSVP.Promise<this>;
save(options?: { adapterOptions?: object }): RSVP.Promise<this>;
/**
* Reload the record from the adapter.
*/
reload(): RSVP.Promise<any>;
reload(options?: { adapterOptions?: object }): RSVP.Promise<this>;
/**
* Get the reference for the specified belongsTo relationship.
*/
@ -547,7 +553,7 @@ export namespace DS {
this: T,
callback: (name: string, details: RelationshipMeta<T>) => void,
binding?: any
): any;
): void;
/**
* Represents the model's class name as a string. This can be used to look up the model's class name through
* `DS.Store`'s modelFor method.
@ -605,14 +611,14 @@ export namespace DS {
static eachRelationship<M extends Model = Model>(
callback: (name: string, details: RelationshipMeta<M>) => void,
binding?: any
): any;
): void;
/**
* Given a callback, iterates over each of the types related to a model,
* invoking the callback with the related type's class. Each type will be
* returned just once, regardless of how many different relationships it has
* with a model.
*/
static eachRelatedType(callback: Function, binding: any): any;
static eachRelatedType(callback: (name: string) => void, binding?: any): void;
/**
* A map whose keys are the attributes of the model (properties
* described by DS.attr) and whose values are the meta object for the
@ -630,26 +636,27 @@ export namespace DS {
* Iterates through the attributes of the model, calling the passed function on each
* attribute.
*/
static eachAttribute(callback: Function, binding: {}): any;
static eachAttribute<Class extends typeof Model, M extends InstanceType<Class>>(
this: Class,
callback: (
name: ModelKeys<M>,
meta: AttributeMeta<M>
) => void,
binding?: any
): void;
/**
* Iterates through the transformedAttributes of the model, calling
* the passed function on each attribute. Note the callback will not be
* called for any attributes that do not have an transformation type.
*/
static eachTransformedAttribute(callback: Function, binding: {}): any;
/**
* Discards any unsaved changes to the given attribute. This feature is not enabled by default. You must enable `ds-rollback-attribute` and be running a canary build.
*/
rollbackAttribute(): any;
/**
* This Ember.js hook allows an object to be notified when a property
* is defined.
*/
didDefineProperty(
proto: {},
key: string,
value: Ember.ComputedProperty<any>
): any;
static eachTransformedAttribute<Class extends typeof Model>(
this: Class,
callback: (
name: ModelKeys<InstanceType<Class>>,
type: keyof TransformRegistry
) => void,
binding?: any
): void;
}
/**
* ### State
@ -700,7 +707,7 @@ export namespace DS {
* Used to get the latest version of all of the records in this array
* from the adapter.
*/
update(): any;
update(): PromiseArray<T>;
/**
* Saves all of the records in the `RecordArray`.
*/
@ -782,7 +789,7 @@ export namespace DS {
/**
* `ids()` returns an array of the record ids in this relationship.
*/
ids(): any[];
ids(): string[];
/**
* The meta data for the has-many relationship.
*/
@ -948,7 +955,7 @@ export namespace DS {
/**
* Get snapshots of the underlying record array
*/
snapshots(): any[];
snapshots(): Snapshot[];
}
class Snapshot<K extends keyof ModelRegistry = any> {
/**
@ -994,14 +1001,19 @@ export namespace DS {
belongsTo<L extends RelationshipsFor<ModelRegistry[K]>>(
keyName: L,
options?: {}
): Snapshot<K>['record'][L] | string | null | undefined;
): Snapshot | null | undefined;
belongsTo<L extends RelationshipsFor<ModelRegistry[K]>>(
keyName: L,
options: { id: true }
): string | null | undefined;
/**
* Returns the current value of a hasMany relationship.
*/
hasMany<L extends RelationshipsFor<ModelRegistry[K]>>(
keyName: L,
options?: { ids: false }
): Array<Snapshot<K>['record'][L]> | undefined;
): Snapshot[] | undefined;
hasMany<L extends RelationshipsFor<ModelRegistry[K]>>(
keyName: L,
options: { ids: true }
@ -1011,21 +1023,21 @@ export namespace DS {
* function on each attribute.
*/
eachAttribute<M extends ModelRegistry[K]>(
callback: (key: keyof M, meta: AttributeMeta<M>) => void,
callback: (key: ModelKeys<M>, meta: AttributeMeta<M>) => void,
binding?: {}
): any;
): void;
/**
* Iterates through all the relationships of the model, calling the passed
* function on each relationship.
*/
eachRelationship<M extends ModelRegistry[K]>(
callback: (key: keyof M, meta: RelationshipMeta<M>) => void,
callback: (key: ModelKeys<M>, meta: RelationshipMeta<M>) => void,
binding?: {}
): any;
): void;
/**
* Serializes the snapshot using the serializer for the model.
*/
serialize(options: {}): {};
serialize<O extends object>(options: O): object;
}
/**
@ -1095,7 +1107,8 @@ export namespace DS {
*/
query<K extends keyof ModelRegistry>(
modelName: K,
query: any
query: object,
options?: { adapterOptions?: object }
): AdapterPopulatedRecordArray<ModelRegistry[K]> &
PromiseArray<ModelRegistry[K]>;
/**
@ -1105,7 +1118,8 @@ export namespace DS {
*/
queryRecord<K extends keyof ModelRegistry>(
modelName: K,
query: any
query: object,
options?: { adapterOptions?: object }
): RSVP.Promise<ModelRegistry[K]>;
/**
* `findAll` asks the adapter's `findAll` method to find the records for the

View File

@ -1,6 +1,7 @@
import Ember from 'ember';
import DS, { ChangedAttributes } from 'ember-data';
import { assertType } from "./lib/assert";
import RSVP from 'rsvp';
const Person = DS.Model.extend({
firstName: DS.attr(),
@ -33,5 +34,26 @@ user.serialize();
user.serialize({ includeId: true });
user.serialize({ includeId: true });
const attributes = user.changedAttributes();
assertType<ChangedAttributes>(attributes);
const attributes: ChangedAttributes = user.changedAttributes();
user.rollbackAttributes(); // $ExpectType void
let destroyResult: RSVP.Promise<typeof user>;
destroyResult = user.destroyRecord();
destroyResult = user.destroyRecord({});
destroyResult = user.destroyRecord({ adapterOptions: {}});
destroyResult = user.destroyRecord({ adapterOptions: { waffles: 'are yummy' }});
user.deleteRecord(); // $ExpectType void
user.unloadRecord(); // $ExpectType void
let jsonified: object;
jsonified = user.toJSON();
jsonified = user.toJSON({ includeId: true });
let reloaded: RSVP.Promise<typeof user>;
reloaded = user.reload();
reloaded = user.reload({});
reloaded = user.reload({ adapterOptions: {} });
reloaded = user.reload({ adapterOptions: { fastAsCanBe: 'yessirree' } });

View File

@ -1,5 +1,7 @@
import Ember from 'ember';
import DS from 'ember-data';
import TransformRegistry from 'ember-data/types/registries/transform';
import { assertType } from './lib/assert';
declare const store: DS.Store;
@ -8,24 +10,65 @@ const Person = DS.Model.extend({
parent: DS.belongsTo('folder', { inverse: 'children' })
});
// $ExpectType void
Person.eachAttribute(() => {});
// $ExpectType void
Person.eachAttribute(() => {}, {});
// $ExpectType void
Person.eachAttribute((name, meta) => {
assertType<'children' | 'parent'>(name);
assertType<{
type: keyof TransformRegistry;
options: object;
name: 'children' | 'parent';
parentType: DS.Model;
isAttribute: true;
}>(meta);
});
// $ExpectType void
Person.eachTransformedAttribute(() => {});
// $ExpectType void
Person.eachTransformedAttribute(() => {}, {});
// $ExpectType void
Person.eachTransformedAttribute((name, type) => {
assertType<'children' | 'parent'>(name);
let t: keyof TransformRegistry = type;
});
const Polymorphic = DS.Model.extend({
paymentMethods: DS.hasMany('payment-method', { polymorphic: true })
});
// $ExpectType void
Polymorphic.eachRelationship(() => '');
// $ExpectType void
Polymorphic.eachRelationship(() => '', {});
// $ExpectType void
Polymorphic.eachRelationship((n, meta) => {
let s: string = n;
let m: 'belongsTo' | 'hasMany' = meta.kind;
});
let p = Polymorphic.create();
// $ExpectType void
p.eachRelationship(() => '');
// $ExpectType void
p.eachRelationship(() => '', {});
// $ExpectType void
p.eachRelationship((n, meta) => {
let s: string = n;
let m: 'belongsTo' | 'hasMany' = meta.kind;
});
// $ExpectType void
Polymorphic.eachRelatedType(() => '');
// $ExpectType void
Polymorphic.eachRelatedType(() => '', {});
// $ExpectType void
Polymorphic.eachRelatedType((name) => {
let s: string = name;
});
export class Comment extends DS.Model {
author = DS.attr('string');
}

View File

@ -1,6 +1,10 @@
import Ember from 'ember';
import DS from 'ember-data';
interface Dict<T> {
[key: string]: T | null | undefined;
}
const JsonApi = DS.JSONAPISerializer.extend({});
const Customized = DS.JSONAPISerializer.extend({
@ -74,14 +78,16 @@ const SerializerUsingSnapshots = DS.RESTSerializer.extend({
DS.Serializer.extend({
serialize(snapshot: DS.Snapshot<'message-for-serializer'>, options: {}) {
let json: any = {
let json: Dict<any> = {
id: snapshot.id
};
// $ExpectType void
snapshot.eachAttribute((key, attribute) => {
json[key] = snapshot.attr(key);
});
// $ExpectType void
snapshot.eachRelationship((key, relationship) => {
if (relationship.kind === 'belongsTo') {
json[key] = snapshot.belongsTo(key, { id: true });

View File

@ -24,6 +24,7 @@ let post = store.createRecord('post', {
});
post.save(); // => POST to '/posts'
post.save({ adapterOptions: { makeItSo: 'number one ' } });
post.save().then(saved => {
assertType<Post>(saved);
});