From 143ee58d484a6fca6f59314ed141a16d55535fc7 Mon Sep 17 00:00:00 2001 From: reppners Date: Thu, 26 Feb 2015 09:53:11 +0100 Subject: [PATCH] + added tests + changed type definition to expose definitions as module and as globally defined namespace var --- js-data-angular/js-data-angular-tests.ts | 78 ++++ js-data-angular/js-data-angular.d.ts | 23 +- js-data/js-data-node-tests.ts | 17 + js-data/js-data-tests.ts | 493 +++++++++++++++++++++++ js-data/js-data.d.ts | 84 ++-- 5 files changed, 652 insertions(+), 43 deletions(-) create mode 100644 js-data-angular/js-data-angular-tests.ts create mode 100644 js-data/js-data-node-tests.ts create mode 100644 js-data/js-data-tests.ts diff --git a/js-data-angular/js-data-angular-tests.ts b/js-data-angular/js-data-angular-tests.ts new file mode 100644 index 000000000..670857dec --- /dev/null +++ b/js-data-angular/js-data-angular-tests.ts @@ -0,0 +1,78 @@ +/// + +interface IUser { + +} + +interface CustomScope extends ng.IScope { + + comments: Array; + user: IUser; + users: Array; +} + +angular.module('myApp') + .controller('commentsCtrl', function ($scope:CustomScope, store:JSData_.DS, Comment:JSData_.DSResourceDefinition, User:JSData_.DSResourceDefinition) { + + Comment.findAll().then(function (comments) { + $scope.comments = comments; + }); + + // shortest version + User.bindOne(1, $scope, 'user'); + +// short version + store.bindOne('user', 1, $scope, 'user'); + +// long version + $scope.$watch(function () { + return store.lastModified('user', 1); + }, function () { + $scope.user = store.get('user', 1); + }); + + var params = { + where: { + age: { + '>': 30 + } + } + }; + +// shortest verions + User.bindAll(params, $scope, 'users'); + +// short version + store.bindAll('user', params, $scope, 'users'); + +// long version + $scope.$watch(function () { + return store.lastModified('user'); + }, function () { + $scope.users = store.filter('user', params); + }); + }); + +angular.module('myApp') + .run(function (DS:JSData_.DS) { + // We don't register the "User" resource + // as a service, so it can only be used + // via DS.('user', ...) + // The advantage here is that this code + // is guaranteed to be executed, and you + // only ever have to inject "DS" + DS.defineResource('user'); + }) + .factory('Comment', function (DS:JSData_.DS) { + // This code won't execute unless you actually + // inject "Comment" somewhere in your code. + // Thanks Angular... + // Some like injecting actual Resource + // definitions, instead of just "DS" + return DS.defineResource('comment'); + }); + +angular.module('myApp') + .config(function (DSProvider:JSData_.DSProvider) { + DSProvider.defaults.basePath = '/myApi'; // etc. + }); \ No newline at end of file diff --git a/js-data-angular/js-data-angular.d.ts b/js-data-angular/js-data-angular.d.ts index a7c035722..851512d15 100644 --- a/js-data-angular/js-data-angular.d.ts +++ b/js-data-angular/js-data-angular.d.ts @@ -6,14 +6,25 @@ /// /// -declare module JSData { +declare module JSData_ { - class ngDS extends DS { + interface DSProvider { + defaults:DSConfiguration; + } - // sync methods - bindAll(scope:ng.IScope, expr:string, resourceName:string, params:DSFilterParams, cb?:(err:DSError, items:Array)=>void):Function; + interface DS { - bindOne(scope:ng.IScope, expr:string, resourceName:string, id:string, cb?:(err:DSError, item:T)=>void):Function; - bindOne(scope:ng.IScope, expr:string, resourceName:string, id:number, cb?:(err:DSError, item:T)=>void):Function; + bindAll(resourceName:string, params:DSFilterParams, scope:ng.IScope, expr:string, cb?:(err:DSError, items:Array)=>void):Function; + + bindOne(resourceName:string, id:string, scope:ng.IScope, expr:string, cb?:(err:DSError, item:T)=>void):Function; + bindOne(resourceName:string, id:number, scope:ng.IScope, expr:string, cb?:(err:DSError, item:T)=>void):Function; + } + + interface DSResourceDefinition { + + bindAll(params:DSFilterParams, scope:ng.IScope, expr:string, cb?:(err:DSError, items:Array)=>void):Function; + + bindOne(id:string, scope:ng.IScope, expr:string, cb?:(err:DSError, item:T)=>void):Function; + bindOne(id:number, scope:ng.IScope, expr:string, cb?:(err:DSError, item:T)=>void):Function; } } \ No newline at end of file diff --git a/js-data/js-data-node-tests.ts b/js-data/js-data-node-tests.ts new file mode 100644 index 000000000..a0b39a161 --- /dev/null +++ b/js-data/js-data-node-tests.ts @@ -0,0 +1,17 @@ +/// + +import JSData = require('js-data'); +//TODO +//import DSRedisAdapter = require('js-data-redis') +var store = new JSData.DS(); + +// register and use http by default for async operations +//TODO +//store.registerAdapter('redis', new DSRedisAdapter(), {default: true}); + +// simplest model definition +var User = store.defineResource('user'); + +User.find(1).then(function (user: any) { + user; // { id: 1, name: 'John' } +}); \ No newline at end of file diff --git a/js-data/js-data-tests.ts b/js-data/js-data-tests.ts new file mode 100644 index 000000000..1cb3e8e72 --- /dev/null +++ b/js-data/js-data-tests.ts @@ -0,0 +1,493 @@ +/// + +interface IUser { + id?: number; + name?: string; + age?: number; + first?: string; + last?: string; + comments?:Array; + profile?:any; +} + +interface IUserWithMethod extends IUser { + fullName?: () => string; +} + +interface IUserWithComputedProperty extends IUser { + fullName?: string; +} + +var store = new JSData.DS(); + +// register and use http by default for async operations +//TODO +//store.registerAdapter('http', new DSHttpAdapter(), {default: true}); + +// simplest model definition +var User = store.defineResource('user'); + +User.find(1).then(function (user:IUser) { + user; // { id: 1, name: 'John' } +}); + +var user:IUser = User.createInstance({name: 'John'}); + +var store = new JSData.DS(); +var User = store.defineResource('user'); +var user:IUser = User.inject({id: 1, name: 'John'}); +var user2:IUser = User.inject({id: 1, age: 30}); + +user; // User { id: 1, name: 'John', age: 30 } +user2; // User { id: 1, name: 'John', age: 30 } +User.get(1); // User { id: 1, name: 'John', age: 30 } +user === user2; // true +user === User.get(1); // true +user2 === User.get(1); // true + +var store = new JSData.DS({ + // set a default lifecycle hook + afterCreate: function () { + } +}); + +var User = store.defineResource({ + name: 'user', + // override the hook for this resource + afterCreate: function () { + } +}); + +User.create({ + name: 'john' +}, { + // override the hook just for this method call + afterCreate: function () { + } +}).then(()=> { + +}); + +var store = new JSData.DS(); + +var UserWithMethod = store.defineResource({ + name: 'user', + methods: { + fullName: function () { + return this.first + ' ' + this.last; + } + } +}); + +var userWithMethod = UserWithMethod.createInstance({first: 'John', last: 'Anderson'}); + +userWithMethod.fullName(); // "John Anderson" + +var store = new JSData.DS(); + +var UserWithComputedProperty = store.defineResource({ + name: 'user', + computed: { + // each function's argument list defines the fields + // that the computed property depends on + fullName: ['first', 'last', function (first: string, last: string) { + return first + ' ' + last; + }], + // shortand, use the array syntax above if you want + // you computed properties to work after you've + // minified your code. Shorthand style won't work when minified + initials: function (first: string, last: string) { + return first.toUpperCase()[0] + '. ' + last.toUpperCase()[0] + '.'; + } + } +}); + +var userWithComputedProperty:IUserWithComputedProperty = UserWithComputedProperty.inject({ + id: 1, + first: 'John', + last: 'Anderson' +}); + +userWithComputedProperty.fullName; // "John Anderson" + +userWithComputedProperty.first = 'Fred'; + +// js-data relies on dirty-checking, so the +// computed property (probably) hasn't been updated yet +userWithComputedProperty.fullName; // "John Anderson" + +// If your browser supports Object.observe this will have no effect +// otherwise it will trigger the dirty-checking +store.digest(); + +userWithComputedProperty.fullName; // "Fred Anderson" + +interface IComment { + comments?: any; + profile?: any; +} + +var aComment:JSData_.DSResourceDefinition = store.defineResource('comment'); + +// Get all comments where comment.userId == 5 +aComment.filter({ + where: { + userId: { + '==': 5 + } + } +}); + +// Get all comments where comment.userId == 5 +aComment.filter({ + userId: 5 +}); + +// Get all comments where comment.userId === 5 +aComment.filter({ + where: { + userId: { + '===': 5 + } + } +}); + +// Get all comments where comment.userId != 5 +aComment.filter({ + where: { + userId: { + '!=': 5 + } + } +}); + +// Get all comments where comment.userId !== 5 +aComment.filter({ + where: { + userId: { + '!==': 5 + } + } +}); + +// Get all users where user.age > 30 +User.filter({ + where: { + age: { + '>': 30 + } + } +}); + +// Get all users where user.age >= 30 +User.filter({ + where: { + age: { + '>=': 30 + } + } +}); + +// Get all users where user.age < 30 +User.filter({ + where: { + age: { + '<': 30 + } + } +}); + +// Get all users where user.name is in "John Anderson" +User.filter({ + where: { + name: { + 'in': 'John Anderson' + } + } +}); + +// Get all users where user.role is in ["admin", "owner"] +User.filter({ + where: { + role: { + 'in': ['admin', 'owner'] + } + } +}); + +// Get all users where user.name is NOT in "John Anderson" +User.filter({ + where: { + name: { + 'notIn': 'John Anderson' + } + } +}); + +// Get all users where user.role is NOT in ["admin", "owner"] +User.filter({ + where: { + role: { + 'notIn': ['admin', 'owner'] + } + } +}); + +// Get all users where user.name contains "John" +User.filter({ + where: { + name: { + 'contains': 'John' + } + } +}); + +// Get all users where user.roles contains "admin" +User.filter({ + where: { + roles: { + 'contains': 'admin' + } + } +}); + +// Sorts users by age in ascending order +User.filter({ + orderBy: 'age' +}); + +// Sorts users by age in descending order +User.filter({ + orderBy: ['age', 'DESC'] +}); + +// Sorts users by age in descending order and then sort by name in ascending order to break a tie +User.filter({ + orderBy: [ + ['age', 'DESC'], + ['name', 'ASC'] + ] +}); + +var PAGE_SIZE = 20; +var currentPage = 1; + +interface IPost { + +} + +var Post:JSData_.DSResourceDefinition; + +// Grab the first "page" of posts +Post.filter({ + offset: PAGE_SIZE * (currentPage - 1), + limit: PAGE_SIZE +}); + +var User = store.defineResource({ + name: 'user', + relations: { + hasMany: { + comment: { + localField: 'comments', + foreignKey: 'userId' + } + }, + hasOne: { + profile: { + localField: 'profile', + foreignKey: 'userId' + } + }, + belongsTo: { + organization: { + localKey: 'organizationId', + localField: 'organization', + + // if you add this to a belongsTo relation + // then js-data will attempt to use + // a nested url structure, e.g. /organization/15/user/4 + parent: true + } + } + } +}); + +var Organization = store.defineResource({ + name: 'organization', + relations: { + hasMany: { + // this is an example of multiple relations + // of the same type to the same resource + user: [ + { + localField: 'users', + foreignKey: 'organizationId' + }, + { + localField: 'owners', + foreignKey: 'organizationId' + } + ] + } + } +}); + +var Profile = store.defineResource({ + name: 'profile', + relations: { + belongsTo: { + user: { + localField: 'user', + localKey: 'userId' + } + } + } +}); + +var OtherComment = store.defineResource({ + name: 'comment', + relations: { + belongsTo: { + user: { + localField: 'user', + localKey: 'userId' + } + } + } +}); + +User.find(10).then(function (user:IUser) { + // let's assume the server only returned the user + user.comments; // undefined + user.profile; // undefined + + User.loadRelations(user, ['comment', 'profile']).then(function (user:IUser) { + user.comments; // array + user.profile; // object + }); +}); + +var OtherOtherComment = store.defineResource({ + name: 'comment', + relations: { + belongsTo: { + post: { + parent: true, + localKey: 'postId', + localField: 'post' + } + } + } +}); + +// The comment isn't in the data store yet, so js-data wouldn't know +// what the id of the parent "post" would be, so we pass it in manually +OtherOtherComment.find(5, {params: {postId: 4}}); // GET /post/4/comment/5 + +// vs + +OtherOtherComment.find(5); // GET /comment/5 + +OtherOtherComment.inject({id: 1, postId: 2}); + +// We don't have to provide the parentKey here +// because js-data found it in the comment +OtherOtherComment.update(1, {content: 'stuff'}); // PUT /post/2/comment/1 + +// If you don't want the nested for just one of the calls then +// you can do the following: +OtherOtherComment.update(1, {content: 'stuff'}, {params: {postId: false}}); // PUT /comment/1 + +var store = new JSData.DS({ + // set the default + beforeCreate: function (resource, data, cb) { + // do something general + cb(null, data); + } +}); + +var User = store.defineResource({ + name: 'user', + // set just for this resource + beforeCreate: function (resource, data, cb) { + // do something more specific to "users" + cb(null, data); + } +}); + +User.create({name: 'John'}, { + // set just for this method call + beforeCreate: function (resource, data, cb) { + // do something specific for this method call + cb(null, data); + } +}); + +module CustomAdapterTest { + + class MyCustomAdapter implements JSData_.IDSAdapter { + + // All of the methods shown here must return a promise + +// "definition" is a resource defintion that would +// be returned by DS#defineResource + +// "options" would be the options argument that +// was passed into the DS method that is calling +// the adapter method + + create(definition:JSData_.DSResourceDefinition, attrs:Object, options:JSData_.DSConfiguration):JSData_.JSDataPromise { + // Must resolve the promise with the created item + + var promise:JSData_.JSDataPromise; + return promise; + } + + find(definition:JSData_.DSResourceDefinition, id:any, options:JSData_.DSConfiguration):JSData_.JSDataPromise { + // Must resolve the promise with the found item + + var promise:JSData_.JSDataPromise; + return promise; + } + + findAll(definition:JSData_.DSResourceDefinition, params:JSData_.DSFilterParams, options:JSData_.DSConfiguration):JSData_.JSDataPromise { + // Must resolve the promise with the found items + + var promise:JSData_.JSDataPromise; + return promise; + } + + update(definition:JSData_.DSResourceDefinition, id:any, attrs:Object, options:JSData_.DSConfiguration):JSData_.JSDataPromise { + // Must resolve the promise with the updated items + + var promise:JSData_.JSDataPromise; + return promise; + } + + updateAll(definition:JSData_.DSResourceDefinition, attrs:Object, params:JSData_.DSFilterParams, options:JSData_.DSConfiguration):JSData_.JSDataPromise { + // Must resolve the promise with the updated items + + var promise:JSData_.JSDataPromise; + return promise; + } + + destroy(definition:JSData_.DSResourceDefinition, id:any, options:JSData_.DSConfiguration):JSData_.JSDataPromise { + // Must return a promise + + var promise:JSData_.JSDataPromise; + return promise; + } + + destroyAll(definition:JSData_.DSResourceDefinition, params:JSData_.DSFilterParams, options:JSData_.DSConfiguration):JSData_.JSDataPromise { + // Must return a promise + + var promise:JSData_.JSDataPromise; + return promise; + } + } + + var store = new JSData.DS(); + store.registerAdapter('mca', new MyCustomAdapter(), {default: true}); + // the data store will now use your custom adapter by default +} \ No newline at end of file diff --git a/js-data/js-data.d.ts b/js-data/js-data.d.ts index ce06edd89..13fc5e691 100644 --- a/js-data/js-data.d.ts +++ b/js-data/js-data.d.ts @@ -9,35 +9,29 @@ /// -declare class JSDataPromise extends Promise { - - // enhanced with finally - finally(finallyCb?: () => U): Promise; -} - /////////////////////////////////////////////////////////////////////////////// // js-data module (js-data.js) /////////////////////////////////////////////////////////////////////////////// -// Support AMD require -declare module 'js-data' { - export = JSData; -} +// defining what exists in JSData and how it looks +declare module JSData_ { -declare module JSData { + class JSDataPromise extends Promise { - class DS { + // enhanced with finally + finally(finallyCb?:() => U):Promise; + } - constructor(config?:DSConfiguration); + //TODO switch to class again when typescript supports open ended class declaration + interface DS { + + new(config?:DSConfiguration):DS; + + // rather undocumented + errors:DSErrors; defaults:DSConfiguration; - //TODO check if still exists - adapters:any; // Object consists of key-values pairs where the key is the name of the adapter and the value is - // the adapter itself. - //TODO check if still exists - errors:DSErrors; - changeHistory(resourceName:string, id?:string):Array; changeHistory(resourceName:string, id?:number):Array; @@ -72,7 +66,7 @@ declare module JSData { find(resourceName:string, id:string, options?:DSAdapterOperationConfiguration):JSDataPromise; find(resourceName:string, id:number, options?:DSAdapterOperationConfiguration):JSDataPromise; - findAll(resourceName:string, params:DSFilterParams, options?:DSAdapterOperationConfiguration):JSDataPromise>; + findAll(resourceName:string, params?:DSFilterParams, options?:DSAdapterOperationConfiguration):JSDataPromise>; get(resourceName:string, id:string, options?:DSConfiguration):T; get(resourceName:string, id:number, options?:DSConfiguration):T; @@ -117,7 +111,7 @@ declare module JSData { refresh(resourceName:string, id:string, options?:DSAdapterOperationConfiguration):JSDataPromise; refresh(resourceName:string, id:number, options?:DSAdapterOperationConfiguration):JSDataPromise; - registerAdapter(adapterId: string, adapter:IDSAdapter, options?:{default: boolean}):void; + registerAdapter(adapterId:string, adapter:IDSAdapter, options?:{default: boolean}):void; save(resourceName:string, id:string, options?:DSSaveConfiguration):JSDataPromise; save(resourceName:string, id:number, options?:DSSaveConfiguration):JSDataPromise; @@ -128,7 +122,7 @@ declare module JSData { update(resourceName:string, id:string, attrs:any, options?:DSSaveConfiguration):JSDataPromise; update(resourceName:string, id:number, attrs:any, options?:DSSaveConfiguration):JSDataPromise; - updateAll(resourceName:string, attrs:any, params:DSFilterParams, options?:DSAdapterOperationConfiguration):JSDataPromise>; + updateAll(resourceName:string, attrs:any, params?:DSFilterParams, options?:DSAdapterOperationConfiguration):JSDataPromise>; } interface DSConfiguration extends IDSResourceLifecycleEventHandlers { @@ -140,8 +134,10 @@ declare module JSData { defaultAdapter?: string; defaultFilter?: (collection:Array, resourceName:string, params:DSFilterParams, options:DSConfiguration)=>Array; eagerEject?: boolean; + // TODO enable when eagerInject in DS#create is implemented + //eagerInject?: boolean; endpoint?: string; - error?: (message?: any, ...optionalParams: any[])=> void; + error?: (message?:any, ...optionalParams:any[])=> void; fallbackAdapters?: Array; findAllFallbackAdapters?: Array; findAllStrategy?: string; @@ -152,10 +148,13 @@ declare module JSData { findInverseLinks?: boolean; findStrategy?: string idAttribute?: string; - ignoredChanges?:Array; + ignoredChanges?: Array; + // TODO ignoreMissing is undocumented + //ignoreMissing: boolean; keepChangeHistory?: boolean; loadFromServer?: boolean; log?: any; + // TODO wait for union types to be supported // log: (message?: any, ...optionalParams: any[])=> void; // log: boolean; maxAge?: number; @@ -166,7 +165,7 @@ declare module JSData { strategy?: string; upsert?: boolean; useClass?: boolean; - useFilter: boolean; + useFilter?: boolean; } interface DSAdapterOperationConfiguration extends DSConfiguration { @@ -224,7 +223,7 @@ declare module JSData { find(id:string, options?:DSAdapterOperationConfiguration):JSDataPromise; find(id:number, options?:DSAdapterOperationConfiguration):JSDataPromise; - findAll(params:DSFilterParams, options?:DSAdapterOperationConfiguration):JSDataPromise>; + findAll(params?:DSFilterParams, options?:DSAdapterOperationConfiguration):JSDataPromise>; get(id:string, options?:DSConfiguration):T; get(id:number, options?:DSConfiguration):T; @@ -256,10 +255,10 @@ declare module JSData { loadRelations(id:string, relations:string, options?:DSAdapterOperationConfiguration):JSDataPromise; loadRelations(id:number, relations:string, options?:DSAdapterOperationConfiguration):JSDataPromise; - loadRelations(instance:Object, relations:string, options?:DSAdapterOperationConfiguration):JSDataPromise; + loadRelations(instance:T, relations:string, options?:DSAdapterOperationConfiguration):JSDataPromise; loadRelations(id:string, relations:Array, options?:DSAdapterOperationConfiguration):JSDataPromise; loadRelations(id:number, relations:Array, options?:DSAdapterOperationConfiguration):JSDataPromise; - loadRelations(instance:Object, relations:Array, options?:DSAdapterOperationConfiguration):JSDataPromise; + loadRelations(instance:T, relations:Array, options?:DSAdapterOperationConfiguration):JSDataPromise; previous(id:string):T; previous(id:number):T; @@ -278,7 +277,7 @@ declare module JSData { update(id:string, attrs:any, options?:DSSaveConfiguration):JSDataPromise; update(id:number, attrs:any, options?:DSSaveConfiguration):JSDataPromise; - updateAll(attrs:any, params:DSFilterParams, options?:DSAdapterOperationConfiguration):JSDataPromise>; + updateAll(attrs:any, params?:DSFilterParams, options?:DSAdapterOperationConfiguration):JSDataPromise>; } interface DSFilterParams { @@ -290,13 +289,13 @@ declare module JSData { offset?: number; orderBy?: any; - // wait for union types to be supported + // TODO wait for union types to be supported //orderBy?: Array>; //orderBy?: Array; //orderBy?: string; sort?: any; - // wait for union types to be supported + // TODO wait for union types to be supported //sort?: string; //sort?: Array; //sort?: Array>; @@ -354,18 +353,19 @@ declare module JSData { } - //TODO check if those are still valid // errors interface DSErrors { // types - IllegalArgumentError:DSError - NonexistentResourceError:DSError - RuntimeError:DSError + IllegalArgumentError:DSError; + IA:DSError; + RuntimeError:DSError; + R:DSError; + NonexistentResourceError:DSError; + NER:DSError; } - //TODO check if those are still valid - interface DSError { + interface DSError extends Error { new (message?:string):DSError; message: string; type: string; @@ -392,3 +392,13 @@ declare module JSData { } } +// declaring the existing global js object +declare var JSData:{ + DS: JSData_.DS +}; + +//Support node require +declare module 'js-data' { + + export = JSData; +} \ No newline at end of file