Skip to content

Instantly share code, notes, and snippets.

@lyschoening
Last active December 26, 2015 05:39
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lyschoening/7102262 to your computer and use it in GitHub Desktop.
Save lyschoening/7102262 to your computer and use it in GitHub Desktop.
Nested Resources in Angular JS. Allows for embedding resources inside the JSON response of other collections as well as for cross-referencing between resources by their URI. See example file for usage (second file). Makes use of Object.defineProperty which requires IE8+
$resourceMinErr = angular.$$minErr('$resource')
angular.module('resources', [])
.provider 'Resource', () ->
provider = @
provider.prefix = '/api/v1'
provider.uriField = 'resource_uri'
class ResourceBase
Api =
$resources: {}
$parse: ($q, object, Resource=null, asInstance=true) ->
if angular.isArray(object)
return (@$parse($q, objectItem, Resource) for objectItem in object)
if typeof object == 'string' and object.charAt(0) == '/' # Is URI
if not Resource? then {resource: Resource} = @$parseUri(object)
return Resource.get(object).$promise # returns a Resource instance with a promise.
if not Resource? # need to resolve path manually.
# TODO fallback for when a nested field does not have a proper uri.
{resource: Resource} = @$parseUri(object[provider.uriField])
for field in Resource.$nestedFields
{embedded, fieldName} = field
if embedded and object[fieldName]? # convert into instance of a nested resource. need cache for that.
object[fieldName] = @$parse($q, object[fieldName])
if asInstance then new Resource(object) else object
$serialize: (object) ->
JSON.stringify object, (key, value) ->
if typeof value == 'object' and value[provider.uriField]? and key != '' # instanceof does not work with ResourceBase
return provider.prefix + value[provider.uriField] # FIXME assumes that the model has already been saved.
return value
$parseUri: (uri) ->
uri = decodeURIComponent(uri)
if uri.indexOf(provider.prefix) == 0
uri = uri.substring(provider.prefix.length)
for path, resource of @$resources
i = 0
params = []
rest = uri
path = path.split(',')
while rest.indexOf(path[i]) == 0
rest = rest.substring(path[i].length + 1)
slashAt = rest.indexOf('/')
if slashAt == -1
params.push(rest)
return {path, params, resource}
params.push(rest.substring(0, slashAt))
rest = rest.substring(slashAt)
i += 1
throw $resourceMinErr('unknownresource', "Resource not defined for: '#{uri}'.")
$fromCamelCase: (str) ->
str.replace(/([a-z][A-Z])/g, (g) -> g[0] + '_' + g[1].toLowerCase())
$toCamelCase: (str) ->
str.replace(/_([a-z])/g, (g) -> return g[1].toUpperCase())
$buildUri: (path, params) ->
# expect(params.length == path.length || params.length == path.length - 1)
uri = ''
for segment, i in path
uri += segment
if i < params.length
uri += '/' + params[i]
uri
$call: ($http, $q, httpConfig, Resource, isArray, instance, instanceMatchUri=false) ->
isInstanceCall = instance?
if not isInstanceCall
value = if isArray then [] else new Resource({})
deferred = $q.defer()
if httpConfig.data?
httpConfig.data = Api.$serialize(httpConfig.data)
$http(httpConfig).then(
(response) ->
{data} = response
# FIXME hack to convert 'null' to null because apparently $http doesn't.
if typeof data == 'string' and data.substring(0,4) == 'null'
data = null
# TODO keep track of promises that need completing.
# TODO attach promise to resource, do more loading inside Resource initialization.
if isArray
if Resource? then for item in data
value.push(Api.$parse($q, item, Resource))
else for item in data
value.push(Api.$parse($q, item))
else
if isInstanceCall and (not instanceMatchUri or data[provider.uriField] == instance.$getUri())
value = instance
if data? then angular.copy(Api.$parse($q, data, Resource, false), value)
if not (isInstanceCall or isArray)
value.$resolved = true
deferred.resolve(value)
value
(response) ->
if not (isInstanceCall or isArray)
value.$resolved = true
deferred.reject(value)
)
if isInstanceCall or isArray
return deferred.promise
value.$resolved = false
value.$promise = deferred.promise
value
$resourceFactory: ($http, $q, path) ->
if Api.$resources[path]?
return Api.$resources[path]
class Resource extends ResourceBase
$getUri: ->
@[provider.uriField]
$getId: ->
Api.$parseUri(@$getUri()).params[0]
$hasUri: ->
@$getUri()?
$then: (success, error, notify) ->
if @$resolved or not @$promise then return success(@)
@$promise.then(success, error, notify)
constructor: (data) ->
angular.copy(data or {}, @)
$save: ->
url = if @$hasUri() then provider.prefix + @$getUri() else provider.prefix + path
httpConfig =
url: url
method: 'POST'
responseType: 'json'
data: @
Api.$call($http, $q, httpConfig, Resource, false, @)
Resource::then = Resource::$then # prefer to have $then since 'then' is reserved in CS
Resource.$nestedFields = []
Resource.$functions = []
Resource.getList = ->
httpConfig =
url: provider.prefix + path
method: 'GET'
Api.$call($http, $q, httpConfig, Resource, true)
Resource.get = (uriOrId) ->
# TODO resolve from cache if possible
if typeof uriOrId == 'string' and uriOrId.charAt(0) == '/' then uri = uriOrId
else uri = Api.$buildUri(path, [uriOrId])
httpConfig =
url: provider.prefix + uri
method: 'GET'
Api.$call($http, $q, httpConfig, Resource, false)
Resource.$childResource = (route) ->
Resource.$nested(route, true)
Resource.$hasFunction = (name, method='POST') ->
route = '/' + Api.$fromCamelCase(name)
Resource::["$#{name}"] = (params) ->
@$then =>
httpConfig =
url: provider.prefix + @$getUri() + route
method: method
data: params
# If response has the same resource_uri, updates current model and return it;
# otherwise, creates a new resource and returns it.
Api.$call($http, $q, httpConfig, Resource, false, @, true)
Resource.$nested = (route, asChildResource=false) ->
# Returns a new (nested) resource if called with asChildResource=true.
if route.charAt(0) == '/' # create collection for nested resource
nestedPath = path.concat([route])
fieldName = route.substring(1)
NestedResource = if asChildResource then Api.$resourceFactory($http, $q, nestedPath) else null
Object.defineProperty Resource::, fieldName,
get: -> {
getList: =>
@$then =>
httpConfig =
url: provider.prefix + @$getUri() + route
method: 'GET'
Api.$call($http, $q, httpConfig, NestedResource, true)
get: (id) =>
@$then =>
httpConfig =
url: provider.prefix + @$getUri() + route + id
method: 'GET'
Api.$call($http, $q, httpConfig, NestedResource, false)
}
set: ->
null
NestedResource
else
Resource.$nestedFields.push({fieldName: route, embedded: true})
Api.$resources[path] = Resource
provider.$get = ($http, $q) ->
(root) ->
Api.$resourceFactory($http, $q, [root])
provider
################################################################
# Example:
Yard = Resource('/yard') # resource model
Yard.$nested('trees') # embedded item or list of items
Chair = Resource('/chair')
Yard.$nested('/chairs') # sub-collection without its own model
# (for many-to-many)
Tree = Resource('/tree')
Tree.$hasFunction('harvest')
# child-collection with its own model
TreeHouse = Tree.$childResource('/treehouse')
yard = Yard.get(1)
# GET /yard/1
# {
# "uri": "/yard/1",
# "trees": [
# "/tree/15", -- reference, looked-up automatically with GET
# {"uri": "/tree/16", "name": "Apple tree"}
# -- full object, resolved to Tree instance
# ]
# }
# GET /tree/16
# {"uri": "/tree/15", "name": "Pine tree"}
yard.chair.getList()
# GET /yard/1/chair
# [{"uri": "/chair/1", ...}, ..]
# -- model inferred from URI
yard.trees[0].treehouse.getList()
# GET /tree/15/treehouse
# [{"uri": "/tree/15/treehouse/1", ...}, ..]
# -- automatically resolved to TreeHouse instance
appleTree = Tree.get(15)
appleTree.then((tree) ->
tree.$harvest({pickAllApples: true})
)
# GET /tree/16
# {"uri": "/tree/16", "name": "Apple tree"}
# POST /tree/16/harvest {pickApples: true}
# -- if the tree object is returned, updates object; otherwise returns normally.
################################################################
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment