Merge pull request #23413 from chriskrycho/ember-type-registries

Ember and Ember Data: use type registries
This commit is contained in:
Ron Buckton
2018-02-10 11:55:41 -08:00
committed by GitHub
12 changed files with 596 additions and 329 deletions
+291 -258
View File
File diff suppressed because it is too large Load Diff
+12
View File
@@ -1,6 +1,11 @@
import Ember from 'ember';
import DS from 'ember-data';
class Session extends Ember.Service {}
declare module '@ember/service' {
interface Registry { 'session': Session; }
}
const JsonApi = DS.JSONAPIAdapter.extend({
// Application specific overrides go here
});
@@ -55,6 +60,13 @@ const UseAjaxOptionsWithOptionalThirdParams = DS.JSONAPIAdapter.extend({
}
});
declare module 'ember-data' {
interface ModelRegistry {
'rootModel': any;
'super-user': any;
}
}
// https://github.com/emberjs/data/blob/c9d8212c857ca78218ad98d11621819b38dba98f/tests/unit/adapters/build-url-mixin/build-url-test.js
const BuildURLAdapter = DS.RESTAdapter.extend({
worksWithOnlyModelNameAndId() {
+8 -2
View File
@@ -3,8 +3,14 @@ import { assertType } from './lib/assert';
class Folder extends DS.Model {
name = DS.attr('string');
children = DS.hasMany<Folder>('folder', { inverse: 'parent' });
parent = DS.belongsTo<Folder>('folder', { inverse: 'children' });
children = DS.hasMany('folder', { inverse: 'parent' });
parent = DS.belongsTo('folder', { inverse: 'children' });
}
declare module 'ember-data' {
interface ModelRegistry {
folder: Folder;
}
}
const folder = Folder.create();
+29 -16
View File
@@ -1,43 +1,56 @@
import DS from 'ember-data';
import { assertType } from './lib/assert';
class Comment extends DS.Model {
class BlogComment extends DS.Model {
text = DS.attr('string');
}
declare module 'ember-data' {
interface ModelRegistry {
'blog-comment': BlogComment;
}
}
class BlogPost extends DS.Model {
title = DS.attr('string');
commentsAsync = DS.hasMany<Comment>('comment');
commentsSync = DS.hasMany<Comment>('comment', { async: false });
commentsAsync = DS.hasMany('blog-comment');
commentsSync = DS.hasMany('blog-comment', { async: false });
}
const post = BlogPost.create();
const blogPost = BlogPost.create();
assertType<DS.PromiseArray<Comment>>(post.get('commentsSync').reload());
assertType<Comment>(post.get('commentsSync').createRecord());
assertType<DS.PromiseArray<BlogComment>>(blogPost.get('commentsSync').reload());
assertType<BlogComment>(blogPost.get('commentsSync').createRecord());
const comment = post.get('commentsSync').get('firstObject');
assertType<Comment | undefined>(comment);
const comment = blogPost.get('commentsSync').get('firstObject');
assertType<BlogComment | undefined>(comment);
if (comment) {
assertType<string>(comment.get('text'));
}
assertType<DS.PromiseArray<Comment>>(post.get('commentsAsync').reload());
assertType<Comment>(post.get('commentsAsync').createRecord());
assertType<Comment | undefined>(post.get('commentsAsync').get('firstObject'));
assertType<DS.PromiseArray<BlogComment>>(blogPost.get('commentsAsync').reload());
assertType<BlogComment>(blogPost.get('commentsAsync').createRecord());
assertType<BlogComment | undefined>(blogPost.get('commentsAsync').get('firstObject'));
const commentAsync = post.get('commentsAsync').get('firstObject');
assertType<Comment | undefined>(commentAsync);
const commentAsync = blogPost.get('commentsAsync').get('firstObject');
assertType<BlogComment | undefined>(commentAsync);
if (commentAsync) {
assertType<string>(commentAsync.get('text'));
}
assertType<boolean>(post.get('commentsAsync').get('isFulfilled'));
assertType<boolean>(blogPost.get('commentsAsync').get('isFulfilled'));
post.get('commentsAsync').then(comments => {
assertType<Comment | undefined>(comments.get('firstObject'));
blogPost.get('commentsAsync').then(comments => {
assertType<BlogComment | undefined>(comments.get('firstObject'));
assertType<string>(comments.get('firstObject')!.get('text'));
});
class PaymentMethod extends DS.Model {}
declare module 'ember-data' {
interface ModelRegistry {
'payment-method': PaymentMethod;
}
}
class Polymorphic extends DS.Model {
paymentMethods = DS.hasMany('payment-method', { polymorphic: true });
}
+8
View File
@@ -1,6 +1,14 @@
import Ember from 'ember';
import DS from 'ember-data';
class MyModel extends DS.Model {}
declare module 'ember-data' {
interface ModelRegistry {
'my-model': MyModel;
}
}
Ember.Route.extend({
model(): any {
return this.store.findAll('my-model');
+7 -1
View File
@@ -7,7 +7,13 @@ class User extends DS.Model {
username = DS.attr('string');
}
let userRef = store.getReference<User>('user', 1);
declare module 'ember-data' {
interface ModelRegistry {
user: User;
}
}
let userRef = store.getReference('user', 1);
// get the record of the reference (null if not yet available)
let user = userRef.value();
+10 -3
View File
@@ -16,14 +16,21 @@ class Comment extends DS.Model {
author = DS.attr('string');
}
class BlogPost extends DS.Model {
class RelationalPost extends DS.Model {
title = DS.attr('string');
tag = DS.attr('string');
comments = DS.hasMany<Comment>('comment', { async: true });
comments = DS.hasMany('comment', { async: true });
relatedPosts = DS.hasMany('post');
}
let blogPost = store.peekRecord<BlogPost>('blog-post', 1);
declare module 'ember-data' {
interface ModelRegistry {
'relational-post': RelationalPost;
comment: Comment;
}
}
let blogPost = store.peekRecord('relational-post', 1);
blogPost!.get('comments').then((comments) => {
// now we can work with the comments
let author: string = comments.get('firstObject')!.get('author');
+2 -1
View File
@@ -4,7 +4,8 @@ import DS from 'ember-data';
const JsonApi = DS.JSONAPISerializer.extend({});
const Customized = DS.JSONAPISerializer.extend({
serialize(snapshot: DS.Snapshot, options: {}) {
serialize(snapshot: DS.Snapshot<'user'>, options: {}) {
const lookup = snapshot.belongsTo('username');
let json: any = this._super(...Array.from(arguments));
json.data.attributes.cost = {
+96 -44
View File
@@ -1,24 +1,30 @@
import Ember from 'ember';
import DS from 'ember-data';
import { assertType } from "./lib/assert";
import { assertType } from './lib/assert';
declare const store: DS.Store;
class Post extends DS.Model {
title = DS.attr('string');
title = DS.attr('string');
}
let post = store.createRecord<Post>('post', {
declare module 'ember-data' {
interface ModelRegistry {
post: Post;
}
}
let post = store.createRecord('post', {
title: 'Rails is Omakase',
body: 'Lorem ipsum'
body: 'Lorem ipsum',
});
post.save(); // => POST to '/posts'
post.save().then((saved) => {
post.save().then(saved => {
assertType<Post>(saved);
});
store.findRecord<Post>('post', 1).then(function(post) {
store.findRecord('post', 1).then(function(post) {
post.get('title'); // => "Rails is Omakase"
post.set('title', 'A new post');
post.save(); // => PATCH to '/posts/1'
@@ -28,21 +34,30 @@ class User extends DS.Model {
username = DS.attr('string');
}
store.queryRecord<User>('user', {}).then(function(user) {
class Author extends User {}
declare module 'ember-data' {
interface ModelRegistry {
'user': User;
'author': Author;
}
}
store.queryRecord('user', {}).then(function(user) {
let username = user.get('username');
console.log(`Currently logged in as ${username}`);
});
store.findAll('blog-post'); // => GET /blog-posts
store.findAll('post'); // => GET /blog-posts
store.findAll('author', { reload: true }).then(function(authors) {
authors.getEach('id'); // ['first', 'second']
});
store.findAll('post', {
adapterOptions: { subscribe: false }
adapterOptions: { subscribe: false },
});
store.findAll('post', { include: 'comments,comments.author' });
store.peekAll('blog-post'); // => no network request
store.peekAll('post'); // => no network request
if (store.hasRecordForId('post', 1)) {
let maybePost = store.peekRecord('post', 1);
@@ -52,16 +67,22 @@ if (store.hasRecordForId('post', 1)) {
}
class Message extends DS.Model {
hasBeenSeen = DS.attr('boolean');
hasBeenSeen = DS.attr('boolean');
}
const messages = store.peekAll<Message>('message');
declare module 'ember-data' {
interface ModelRegistry {
message: Message;
}
}
const messages = store.peekAll('message');
messages.forEach(function(message) {
message.set('hasBeenSeen', true);
});
messages.save();
const people = store.peekAll('person');
const people = store.peekAll('user');
people.get('isUpdating'); // false
people.update().then(function() {
people.get('isUpdating'); // false
@@ -70,53 +91,84 @@ people.get('isUpdating'); // true
const MyRoute = Ember.Route.extend({
model(params: any): any {
return this.store.findRecord('post', params.post_id, {include: 'comments,comments.author'});
return this.store.findRecord('post', params.post_id, {
include: 'comments,comments.author',
});
},
});
// Store is injectable via `inject` and resolves to `DS.Store`.
const SomeComponent = Ember.Component.extend({
store: Ember.inject.service('store'),
lookUpUsers() {
assertType<User>(this.get('store').findRecord('user', 123));
assertType<DS.PromiseArray<User>>(this.get('store').findAll('user'));
}
});
// GET to /users?filter[email]=tomster@example.com
const tom = store.query('user', {
filter: {
email: 'tomster@example.com'
}
}).then(function(users) {
return users.get("firstObject");
});
const tom = store
.query('user', {
filter: {
email: 'tomster@example.com',
},
})
.then(function(users) {
return users.get('firstObject');
});
// GET /users?isAdmin=true
const admins = store.query('user', { isAdmin: true });
admins.then(function() {
console.log(admins.get("length")); // 42
console.log(admins.get('length')); // 42
});
admins.update().then(function() {
admins.get('isUpdating'); // false
console.log(admins.get("length")); // 123
console.log(admins.get('length')); // 123
});
store.push({
data: [{
id: 1,
type: 'album',
attributes: {
title: 'Fewer Moving Parts',
artist: 'David Bazan',
songCount: 10
data: [
{
id: 1,
type: 'album',
attributes: {
title: 'Fewer Moving Parts',
artist: 'David Bazan',
songCount: 10,
},
relationships: {},
},
relationships: {}
}, {
id: 2,
type: 'album',
attributes: {
title: 'Calgary b/w I Can\'t Make You Love Me/Nick Of Time',
artist: 'Bon Iver',
songCount: 2
{
id: 2,
type: 'album',
attributes: {
title: "Calgary b/w I Can't Make You Love Me/Nick Of Time",
artist: 'Bon Iver',
songCount: 2,
},
relationships: {},
},
relationships: {}
}]
],
});
class UserAdapter extends DS.Adapter { }
class UserSerializer extends DS.Serializer { }
class UserAdapter extends DS.Adapter {
thisAdapterOnlyMethod(): void {}
}
class UserSerializer extends DS.Serializer {
thisSerializerOnlyMethod(): void {}
}
assertType<UserAdapter>(store.adapterFor<UserAdapter>('user'));
assertType<UserSerializer>(store.serializerFor<UserSerializer>('user'));
declare module 'ember-data' {
interface AdapterRegistry {
user: UserAdapter;
}
interface SerializerRegistry {
user: UserSerializer;
}
}
assertType<UserAdapter>(store.adapterFor('user'));
assertType<UserSerializer>(store.serializerFor('user'));
+1
View File
@@ -1,6 +1,7 @@
{
"extends": "dtslint/dt.json",
"rules": {
"strict-export-declare-modifiers": false,
// Heavy use of Function type in this older package.
"ban-types": false,
"jsdoc-format": false,
+97 -2
View File
@@ -18,6 +18,9 @@ declare module 'ember' {
import Rsvp from 'rsvp';
import { TemplateFactory } from 'htmlbars-inline-precompile';
import { Registry as ServiceRegistry } from '@ember/service';
import { Registry as ControllerRegistry } from '@ember/controller';
// Get an alias to the global Array type to use in inner scope below.
type GlobalArray<T> = T[];
@@ -2303,12 +2306,18 @@ declare module 'ember' {
* Creates a property that lazily looks up another controller in the container.
* Can only be used when defining another controller.
*/
function controller(name?: string): ComputedProperty<Controller>;
function controller(): ComputedProperty<Ember.Controller>;
function controller<K extends keyof ControllerRegistry>(
name: K
): ComputedProperty<ControllerRegistry[K]>;
/**
* Creates a property that lazily looks up a service in the container. There
* are no restrictions as to what objects a service can be injected into.
*/
function service(name?: string): ComputedProperty<Service>;
function service(): ComputedProperty<Ember.Service>;
function service<K extends keyof ServiceRegistry>(
name: K
): ComputedProperty<ServiceRegistry[K]>;
}
namespace ENV {
const EXTEND_PROTOTYPES: typeof Ember.EXTEND_PROTOTYPES;
@@ -3279,6 +3288,84 @@ declare module 'ember' {
function expandProperties(pattern: string, callback: (expanded: string) => void): void;
}
type RouteModel = object | string | number;
// https://emberjs.com/api/ember/2.18/classes/RouterService
/**
* The Router service is the public API that provides component/view layer access to the router.
*/
class RouterService extends Ember.Service {
//
/**
* Determines whether a route is active.
*
* @param routeName the name of the route
* @param models the model(s) or identifier(s) to be used while
* transitioning to the route
* @param options optional hash with a queryParams property containing a
* mapping of query parameters
*/
isActive(routeName: string, models: RouteModel, options?: { queryParams: object }): boolean;
isActive(routeName: string, modelsA: RouteModel, modelsB: RouteModel, options?: { queryParams: object }): boolean;
isActive(routeName: string, modelsA: RouteModel, modelsB: RouteModel, modelsC: RouteModel, options?: { queryParams: object }): boolean;
isActive(routeName: string, modelsA: RouteModel, modelsB: RouteModel, modelsC: RouteModel, modelsD: RouteModel, options?: { queryParams: object }): boolean;
// https://emberjs.com/api/ember/2.18/classes/RouterService/methods/isActive?anchor=replaceWith
/**
* Transition into another route while replacing the current URL, if
* possible. The route may be either a single route or route path.
*
* @param routeNameOrUrl the name of the route or a URL
* @param models the model(s) or identifier(s) to be used while
* transitioning to the route.
* @param options optional hash with a queryParams property
* containing a mapping of query parameters
* @returns the Transition object associated with this attempted transition
*/
replaceWith(routeNameOrUrl: string, models: RouteModel, options?: { queryParams: object }): Ember.Transition;
replaceWith(routeNameOrUrl: string, modelsA: RouteModel, modelsB: RouteModel, options?: { queryParams: object }): Ember.Transition;
replaceWith(routeNameOrUrl: string, modelsA: RouteModel, modelsB: RouteModel, modelsC: RouteModel, options?: { queryParams: object }): Ember.Transition;
replaceWith(routeNameOrUrl: string, modelsA: RouteModel, modelsB: RouteModel, modelsC: RouteModel, modelsD: RouteModel, options?: { queryParams: object }): Ember.Transition;
// https://emberjs.com/api/ember/2.18/classes/RouterService/methods/isActive?anchor=transitionTo
/**
* Transition the application into another route. The route may be
* either a single route or route path
*
* @param routeNameOrUrl the name of the route or a URL
* @param models the model(s) or identifier(s) to be used while
* transitioning to the route.
* @param options optional hash with a queryParams property
* containing a mapping of query parameters
* @returns the Transition object associated with this attempted transition
*/
transitionTo(routeNameOrUrl: string, models: RouteModel, options?: { queryParams: object }): Ember.Transition;
transitionTo(routeNameOrUrl: string, modelsA: RouteModel, modelsB: RouteModel, options?: { queryParams: object }): Ember.Transition;
transitionTo(routeNameOrUrl: string, modelsA: RouteModel, modelsB: RouteModel, modelsC: RouteModel, options?: { queryParams: object }): Ember.Transition;
transitionTo(routeNameOrUrl: string, modelsA: RouteModel, modelsB: RouteModel, modelsC: RouteModel, modelsD: RouteModel, options?: { queryParams: object }): Ember.Transition;
// https://emberjs.com/api/ember/2.18/classes/RouterService/methods/isActive?anchor=urlFor
/**
* Generate a URL based on the supplied route name.
*
* @param routeName the name of the route or a URL
* @param models the model(s) or identifier(s) to be used while
* transitioning to the route.
* @param options optional hash with a queryParams property containing
* a mapping of query parameters
* @returns the string representing the generated URL
*/
urlFor(routeName: string, models: RouteModel, options?: { queryParams: object }): string;
urlFor(routeName: string, modelsA: RouteModel, modelsB: RouteModel, options?: { queryParams: object }): string;
urlFor(routeName: string, modelsA: RouteModel, modelsB: RouteModel, modelsC: RouteModel, options?: { queryParams: object }): string;
urlFor(routeName: string, modelsA: RouteModel, modelsB: RouteModel, modelsC: RouteModel, modelsD: RouteModel, options?: { queryParams: object }): string;
}
module '@ember/service' {
interface Registry {
'router': RouterService;
}
}
export default Ember;
}
@@ -3362,6 +3449,10 @@ declare module '@ember/controller' {
import Ember from 'ember';
export default class Controller extends Ember.Controller { }
export const inject: typeof Ember.inject.controller;
// A type registry for Ember `Controller`s. Meant to be declaration-merged
// so string lookups resolve to the correct type.
export interface Registry {}
}
declare module '@ember/debug' {
@@ -3599,6 +3690,10 @@ declare module '@ember/service' {
import Ember from 'ember';
export default class Service extends Ember.Service { }
export const inject: typeof Ember.inject.service;
// A type registry for Ember `Service`s. Meant to be declaration-merged so
// string lookups resolve to the correct type.
interface Registry {}
}
declare module '@ember/string' {
+35 -2
View File
@@ -8,9 +8,21 @@ class ApplicationController extends Ember.Controller {
transitionToLogin() {}
}
declare module '@ember/service' {
interface Registry {
'auth': AuthService;
}
}
declare module '@ember/controller' {
interface Registry {
'application': ApplicationController;
}
}
class LoginRoute extends Ember.Route {
auth = Ember.inject.service('authentication') as Ember.ComputedProperty<AuthService>;
application = Ember.inject.controller() as Ember.ComputedProperty<ApplicationController>;
auth = Ember.inject.service('auth');
application = Ember.inject.controller('application');
didTransition() {
if (!this.get('auth').get('isAuthenticated')) {
@@ -18,3 +30,24 @@ class LoginRoute extends Ember.Route {
}
}
}
// New module injection style.
import Controller, { inject as controller } from '@ember/controller';
import Service, { inject as service } from '@ember/service';
import { assertType } from './lib/assert';
class ComponentInjection extends Ember.Component {
applicationController = controller('application');
auth = service('auth');
router = service('router');
misc = service();
testem() {
assertType<Ember.Service>(this.get('misc'));
const url = this.get('router').urlFor('some-route', 1, 2, 3, { queryParams: { seriously: 'yes' } });
assertType<string>(url);
if (!this.get('auth').isAuthenticated) {
this.get('applicationController').transitionToLogin();
}
}
}