diff --git a/app/adapters/repo.js b/app/adapters/repo.js new file mode 100644 index 00000000..560506ee --- /dev/null +++ b/app/adapters/repo.js @@ -0,0 +1,24 @@ +import V3Adapter from 'travis/adapters/v3'; + +export default V3Adapter.extend({ + buildUrl(modelName, id, snapshot, requestType, query) { + var url = this._super(...arguments); + + return url; + }, + + ajaxOptions(url, type, options) { + var hash = options || {}; + if(!hash.data) { + hash.data = {}; + } + + if(hash.data.include) { + hash.data.include += ',repository.last_build,build.commit'; + } else { + hash.data.include = 'repository.last_build,build.commit'; + } + + return this._super(url, type, hash); + } +}); diff --git a/app/adapters/v3.js b/app/adapters/v3.js new file mode 100644 index 00000000..fd18b613 --- /dev/null +++ b/app/adapters/v3.js @@ -0,0 +1,60 @@ +import Ember from 'ember'; +import DS from 'ember-data'; +import config from 'travis/config/environment'; + +export default DS.RESTAdapter.extend({ + auth: Ember.inject.service(), + host: config.apiEndpoint, + + defaultSerializer: '-repo', + + sortQueryParams: false, + coalesceFindRequests: false, + headers: { + 'Travis-API-Version': '3', + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + + ajaxOptions: function(url, type, options) { + var hash = this._super(...arguments); + + hash.headers = hash.headers || {}; + + var token; + if(token = this.get('auth').token()) { + hash.headers['Authorization'] = "token " + token; + } + + return hash; + }, + + // TODO: I shouldn't override this method as it's private, a better way would + // be to create my own URL generator + _buildURL: function(modelName, id) { + var url = []; + var host = get(this, 'host'); + var prefix = this.urlPrefix(); + var path; + + if (modelName) { + path = this.pathForType(modelName, id); + if (path) { url.push(path); } + } + + if (id) { url.push(encodeURIComponent(id)); } + if (prefix) { url.unshift(prefix); } + + url = url.join('/'); + if (!host && url && url.charAt(0) !== '/') { + url = '/' + url; + } + + return url; + }, + + pathForType: function(modelName, id) { + var underscored = Ember.String.underscore(modelName); + return id ? underscored : Ember.String.pluralize(underscored); + } +}); diff --git a/app/models/build.coffee b/app/models/build.coffee index 10f455ab..336cda61 100644 --- a/app/models/build.coffee +++ b/app/models/build.coffee @@ -21,7 +21,7 @@ Build = Model.extend DurationCalculations, eventType: DS.attr('string') repo: DS.belongsTo('repo', async: true) - commit: DS.belongsTo('commit', async: true) + commit: DS.belongsTo('commit') jobs: DS.hasMany('job', async: true) config: (-> diff --git a/app/models/repo.coffee b/app/models/repo.coffee index 9fc2d8cc..bace43f2 100644 --- a/app/models/repo.coffee +++ b/app/models/repo.coffee @@ -11,31 +11,12 @@ Repo = Model.extend slug: DS.attr() description: DS.attr() private: DS.attr('boolean') - lastBuildNumber: DS.attr('number') - lastBuildState: DS.attr() - lastBuildStartedAt: DS.attr() - lastBuildFinishedAt: DS.attr() githubLanguage: DS.attr() - _lastBuildDuration: DS.attr('number') - lastBuildLanguage: DS.attr() active: DS.attr() - lastBuildId: DS.attr('number') - lastBuildHash: (-> - { - id: @get('lastBuildId') - number: @get('lastBuildNumber') - repo: this - } - ).property('lastBuildId', 'lastBuildNumber') - - lastBuild: (-> - if id = @get('lastBuildId') - @store.find('build', id) - @store.recordForId('build', id) - ).property('lastBuildId') + lastBuild: DS.belongsTo('build') withLastBuild: -> - @filter( (repo) -> repo.get('lastBuildId') ) + @filter( (repo) -> repo.get('lastBuild') ) sshKey: (-> @store.find('ssh_key', @get('id')) @@ -150,7 +131,10 @@ Repo.reopenClass @find() accessibleBy: (store, login) -> - repos = store.query('repo', { member: login, orderBy: 'name' }) + # this fires only for authenticated users and with API v3 that means getting + # only repos of currently logged in owner, but in the future it would be + # nice to not use that as it may change in the future + repos = store.query('repo', { 'repository.active': 'true' }) repos.then () -> repos.set('isLoaded', true) @@ -169,7 +153,7 @@ Repo.reopenClass withLastBuild: (store) -> repos = store.filter('repo', {}, (build) -> - build.get('lastBuildId') + build.get('lastBuild') ) repos.then () -> @@ -177,20 +161,16 @@ Repo.reopenClass repos - bySlug: (store, slug) -> - # first check if there is a repo with a given slug already ordered - repos = store.peekAll('repo').filterBy('slug', slug) - if repos.get('length') > 0 - repos - else - store.query('repo', { slug: slug }) - fetchBySlug: (store, slug) -> - repos = @bySlug(store, slug) + repos = store.peekAll('repo').filterBy('slug', slug) if repos.get('length') > 0 repos.get('firstObject') else - repos.then (repos) -> + adapter = store.adapterFor('repo') + modelClass = store.modelFor('repo') + adapter.findRecord(store, modelClass, slug).then (resourceHash) -> + store.push(store.normalize('repo', resourceHash)); + , -> error = new Error('repo not found') error.slug = slug Ember.get(repos, 'firstObject') || throw(error) diff --git a/app/routes/repo.coffee b/app/routes/repo.coffee index ef9a1b95..eb80dcc4 100644 --- a/app/routes/repo.coffee +++ b/app/routes/repo.coffee @@ -1,7 +1,9 @@ `import TravisRoute from 'travis/routes/basic'` `import Repo from 'travis/models/repo'` +`import Ember from 'ember'` Route = TravisRoute.extend + store: Ember.inject.service() titleToken: (model) -> model.get('slug') @@ -11,7 +13,7 @@ Route = TravisRoute.extend setupController: (controller, model) -> # TODO: if repo is just a data hash with id and slug load it # as incomplete record - model = @store.find('repo', model.id) if model && !model.get + model = @get('store').find('repo', model.id) if model && !model.get controller.set('repo', model) serialize: (repo) -> @@ -21,7 +23,7 @@ Route = TravisRoute.extend model: (params) -> slug = "#{params.owner}/#{params.name}" - Repo.fetchBySlug(@store, slug) + Repo.fetchBySlug(@get('store'), slug) resetController: -> @controllerFor('repo').deactivate() diff --git a/app/routes/repo/index.coffee b/app/routes/repo/index.coffee index 0715b315..6c32b7d3 100644 --- a/app/routes/repo/index.coffee +++ b/app/routes/repo/index.coffee @@ -6,7 +6,7 @@ Route = TravisRoute.extend @controllerFor('repo').activate('current') renderTemplate: -> - if @modelFor('repo').get('lastBuildId') + if @modelFor('repo').get('lastBuild') @render 'build' else @render 'builds/not_found' diff --git a/app/serializers/application.coffee b/app/serializers/application.coffee index b1916d37..1e23b40b 100644 --- a/app/serializers/application.coffee +++ b/app/serializers/application.coffee @@ -1,7 +1,7 @@ `import DS from 'ember-data'` +`import V2FallbackSerializer from 'travis/serializers/v2_fallback'` -Serializer = DS.ActiveModelSerializer.extend - defaultSerializer: 'application' - serializer: 'application' +Serializer = V2FallbackSerializer.extend + isNewSerializerAPI: true `export default Serializer` diff --git a/app/serializers/build.coffee b/app/serializers/build.coffee index c663c045..3212b68d 100644 --- a/app/serializers/build.coffee +++ b/app/serializers/build.coffee @@ -1,19 +1,19 @@ `import Ember from 'ember'` -`import ApplicationSerializer from 'travis/serializers/application'` +`import V2FallbackSerializer from 'travis/serializers/v2_fallback'` -Serializer = ApplicationSerializer.extend +Serializer = V2FallbackSerializer.extend + isNewSerializerAPI: true attrs: { - repo: { key: 'repository_id' } _config: { key: 'config' } _finishedAt: { key: 'finished_at' } _startedAt: { key: 'started_at' } _duration: { key: 'duration' } } - extractSingle: (store, primaryType, rawPayload, recordId) -> - if commit = rawPayload.commit - rawPayload.commits = [commit] - - @_super(store, primaryType, rawPayload, recordId) + keyForV2Relationship: (key, typeClass, method) -> + if key == 'repo' + 'repository_id' + else + @_super.apply(this, arguments) `export default Serializer` diff --git a/app/serializers/repo.coffee b/app/serializers/repo.coffee index ebdb0287..5bba95f8 100644 --- a/app/serializers/repo.coffee +++ b/app/serializers/repo.coffee @@ -1,7 +1,8 @@ `import Ember from 'ember'` -`import ApplicationSerializer from 'travis/serializers/application'` +`import V2FallbackSerializer from 'travis/serializers/v2_fallback'` -Serializer = ApplicationSerializer.extend +Serializer = V2FallbackSerializer.extend + isNewSerializerAPI: true attrs: { _lastBuildDuration: { key: 'last_build_duration' } } diff --git a/app/serializers/v2_fallback.js b/app/serializers/v2_fallback.js new file mode 100644 index 00000000..eb687e88 --- /dev/null +++ b/app/serializers/v2_fallback.js @@ -0,0 +1,41 @@ +import Ember from 'ember'; +import V3Serializer from 'travis/serializers/v3'; + +export default V3Serializer.extend({ + isNewSerializerAPI: true, + + extractRelationships(modelClass, resourceHash) { + if(resourceHash['@type']) { + return this._super(...arguments); + } else { + let relationships = {}; + + modelClass.eachRelationship((key, relationshipMeta) => { + // V2 API payload + let relationship = null; + let relationshipKey = this.keyForV2Relationship(key, relationshipMeta.kind, 'deserialize'); + + if (resourceHash.hasOwnProperty(relationshipKey)) { + let data = null; + let relationshipHash = resourceHash[relationshipKey]; + if (relationshipMeta.kind === 'belongsTo') { + data = this.extractRelationship(relationshipMeta.type, relationshipHash); + } else if (relationshipMeta.kind === 'hasMany') { + data = relationshipHash.map((item) => this.extractRelationship(relationshipMeta.type, item)); + } + relationship = { data }; + } + + if (relationship) { + relationships[key] = relationship; + } + }); + + return relationships; + } + }, + + keyForV2Relationship(key, typeClass, method) { + return key.underscore() + '_id'; + } +}); diff --git a/app/serializers/v3.js b/app/serializers/v3.js new file mode 100644 index 00000000..05b1a55f --- /dev/null +++ b/app/serializers/v3.js @@ -0,0 +1,105 @@ +import Ember from 'ember'; +import DS from 'ember-data'; + +export default DS.JSONSerializer.extend({ + isNewSerializerAPI: true, + + extractRelationship() { + let relationshipHash = this._super(...arguments); + if(relationshipHash && relationshipHash['@type']) { + relationshipHash.type = relationshipHash['@type']; + } + return relationshipHash; + }, + + extractRelationships() { + let relationships = this._super(...arguments); + return relationships; + }, + + keyForRelationship(key, typeClass, method) { + if(key && key.underscore) { + return key.underscore(); + } else { + return key; + } + }, + + extractAttributes() { + let attributes = this._super(...arguments); + for(let key in attributes) { + if(key.startsWith('@')) { + delete attributes.key; + } + } + + return attributes; + }, + + normalizeArrayResponse(store, primaryModelClass, payload, id, requestType) { + let documentHash = { + data: null, + included: [] + }; + + let meta = this.extractMeta(store, primaryModelClass, payload); + if (meta) { + Ember.assert('The `meta` returned from `extractMeta` has to be an object, not "' + Ember.typeOf(meta) + '".', Ember.typeOf(meta) === 'object'); + documentHash.meta = meta; + } + + let items, type; + if(type = payload['@type']) { + items = payload[type]; + } else { + items = payload[primaryModelClass.modelName + 's']; + } + + documentHash.data = items.map((item) => { + let { data, included } = this.normalize(primaryModelClass, item); + if (included) { + documentHash.included.push(...included); + } + return data; + }); + + return documentHash; + }, + + normalize(modelClass, resourceHash) { + let { data, included } = this._super(...arguments); + if(!included) { + included = []; + } + let store = this.store; + + if(data.relationships) { + Object.keys(data.relationships).forEach(function (key) { + let relationship = data.relationships[key]; + let process = function(data) { + if(data['@representation'] !== 'standard') { + return; + } + let type = data['@type']; + let serializer = store.serializerFor(type); + let modelClass = store.modelFor(type); + let normalized = serializer.normalize(modelClass, data); + included.push(normalized.data); + if(normalized.included) { + normalized.included.forEach(function(item) { + included.push(item); + }); + } + }; + + if(Array.isArray(relationship)) { + relationship.forEach(process); + } else if(relationship && relationship.data) { + process(relationship.data); + } + }); + } + + return { data, included }; + } +}); diff --git a/app/templates/components/repos-list-item.hbs b/app/templates/components/repos-list-item.hbs index 9967ca73..77e8dc00 100644 --- a/app/templates/components/repos-list-item.hbs +++ b/app/templates/components/repos-list-item.hbs @@ -1,5 +1,5 @@ -
-

+
+

{{#if repo.slug}} {{#link-to "repo" repo}} {{status-icon status=repo.lastBuildState}} @@ -7,32 +7,31 @@ {{/link-to}} {{/if}}

- {{#with repo.lastBuildHash as lastBuild}} - {{#if repo.slug}} - {{#if lastBuild.id}} -

- {{#link-to "build" repo lastBuild.id}} - - {{lastBuild.number}} - {{/link-to}} -

- {{/if}} + + {{#if repo.slug}} + {{#if repo.lastBuild.id}} +

+ {{#link-to "build" repo repo.lastBuild.id}} + + {{lastBuild.number}} + {{/link-to}} +

{{/if}} - {{/with}} + {{/if}}

Duration: - - {{format-duration repo.lastBuildDuration}} + + {{format-duration repo.lastBuild.duration}}

Finished: - - {{format-time repo.lastBuildFinishedAt}} + + {{format-time repo.lastBuild.finishedAt}}