Last active
December 26, 2015 05:39
-
-
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+
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$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 | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
################################################################ | |
# 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