spine-rails 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +12 -3
- data/build/update.sh +9 -0
- data/lib/generators/spine/model/model_generator.rb +1 -1
- data/lib/generators/spine/new/new_generator.rb +14 -14
- data/lib/generators/spine/scaffold/scaffold_generator.rb +5 -5
- data/lib/generators/spine/scaffold/templates/controller.coffee.erb +1 -1
- data/lib/spine/generators.rb +5 -5
- data/lib/spine/rails/version.rb +2 -2
- data/spine-rails.gemspec +2 -1
- data/vendor/assets/javascripts/spine.coffee +640 -0
- data/vendor/assets/javascripts/spine/ajax.coffee +237 -0
- data/vendor/assets/javascripts/spine/list.coffee +43 -0
- data/vendor/assets/javascripts/spine/local.coffee +16 -0
- data/vendor/assets/javascripts/spine/manager.coffee +84 -0
- data/vendor/assets/javascripts/spine/relation.coffee +136 -0
- data/vendor/assets/javascripts/spine/route.coffee +148 -0
- metadata +41 -17
- data/app/assets/javascripts/json2.js +0 -485
- data/app/assets/javascripts/spine.js +0 -808
- data/app/assets/javascripts/spine/ajax.js +0 -290
- data/app/assets/javascripts/spine/list.js +0 -70
- data/app/assets/javascripts/spine/local.js +0 -28
- data/app/assets/javascripts/spine/manager.js +0 -154
- data/app/assets/javascripts/spine/relation.js +0 -222
- data/app/assets/javascripts/spine/route.js +0 -197
- data/app/assets/javascripts/spine/tabs.js +0 -66
- data/app/assets/javascripts/spine/tmpl.js +0 -22
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
Spine = @Spine or require('spine')
|
|
2
|
+
$ = Spine.$
|
|
3
|
+
Model = Spine.Model
|
|
4
|
+
Queue = $({})
|
|
5
|
+
|
|
6
|
+
Ajax =
|
|
7
|
+
getURL: (object) ->
|
|
8
|
+
object and object.url?() or object.url
|
|
9
|
+
|
|
10
|
+
getScope: (object) ->
|
|
11
|
+
scope = object and object.scope?() or object.scope
|
|
12
|
+
if scope? and scope.charAt(0) is '/'
|
|
13
|
+
scope = scope.substring(1)
|
|
14
|
+
scope
|
|
15
|
+
|
|
16
|
+
generateURL: (object, args...) ->
|
|
17
|
+
if object.className
|
|
18
|
+
collection = object.className.toLowerCase() + 's'
|
|
19
|
+
scope = Ajax.getScope(object)
|
|
20
|
+
else
|
|
21
|
+
if typeof object.constructor.url is 'string'
|
|
22
|
+
collection = object.constructor.url
|
|
23
|
+
collection = collection.substring(1) if collection.charAt(0) is '/'
|
|
24
|
+
else
|
|
25
|
+
collection = object.constructor.className.toLowerCase() + 's'
|
|
26
|
+
scope = Ajax.getScope(object) or Ajax.getScope(object.constructor)
|
|
27
|
+
args.unshift(collection)
|
|
28
|
+
if scope?
|
|
29
|
+
args.unshift(scope)
|
|
30
|
+
args.unshift(Model.host)
|
|
31
|
+
args.join('/')
|
|
32
|
+
|
|
33
|
+
enabled: true
|
|
34
|
+
|
|
35
|
+
disable: (callback) ->
|
|
36
|
+
if @enabled
|
|
37
|
+
@enabled = false
|
|
38
|
+
try
|
|
39
|
+
do callback
|
|
40
|
+
catch e
|
|
41
|
+
throw e
|
|
42
|
+
finally
|
|
43
|
+
@enabled = true
|
|
44
|
+
else
|
|
45
|
+
do callback
|
|
46
|
+
|
|
47
|
+
queue: (request) ->
|
|
48
|
+
if request then Queue.queue(request) else Queue.queue()
|
|
49
|
+
|
|
50
|
+
clearQueue: ->
|
|
51
|
+
@queue []
|
|
52
|
+
|
|
53
|
+
class Base
|
|
54
|
+
defaults:
|
|
55
|
+
dataType: 'json'
|
|
56
|
+
processData: false
|
|
57
|
+
headers: {'X-Requested-With': 'XMLHttpRequest'}
|
|
58
|
+
|
|
59
|
+
queue: Ajax.queue
|
|
60
|
+
|
|
61
|
+
ajax: (params, defaults) ->
|
|
62
|
+
$.ajax @ajaxSettings(params, defaults)
|
|
63
|
+
|
|
64
|
+
ajaxQueue: (params, defaults) ->
|
|
65
|
+
jqXHR = null
|
|
66
|
+
deferred = $.Deferred()
|
|
67
|
+
promise = deferred.promise()
|
|
68
|
+
return promise unless Ajax.enabled
|
|
69
|
+
settings = @ajaxSettings(params, defaults)
|
|
70
|
+
|
|
71
|
+
request = (next) ->
|
|
72
|
+
jqXHR = $.ajax(settings)
|
|
73
|
+
.done(deferred.resolve)
|
|
74
|
+
.fail(deferred.reject)
|
|
75
|
+
.then(next, next)
|
|
76
|
+
|
|
77
|
+
promise.abort = (statusText) ->
|
|
78
|
+
return jqXHR.abort(statusText) if jqXHR
|
|
79
|
+
index = $.inArray(request, @queue())
|
|
80
|
+
@queue().splice(index, 1) if index > -1
|
|
81
|
+
deferred.rejectWith(
|
|
82
|
+
settings.context or settings,
|
|
83
|
+
[promise, statusText, '']
|
|
84
|
+
)
|
|
85
|
+
promise
|
|
86
|
+
|
|
87
|
+
@queue request
|
|
88
|
+
promise
|
|
89
|
+
|
|
90
|
+
ajaxSettings: (params, defaults) ->
|
|
91
|
+
$.extend({}, @defaults, defaults, params)
|
|
92
|
+
|
|
93
|
+
class Collection extends Base
|
|
94
|
+
constructor: (@model) ->
|
|
95
|
+
|
|
96
|
+
find: (id, params) ->
|
|
97
|
+
record = new @model(id: id)
|
|
98
|
+
@ajaxQueue(
|
|
99
|
+
params,
|
|
100
|
+
type: 'GET',
|
|
101
|
+
url: Ajax.getURL(record)
|
|
102
|
+
).done(@recordsResponse)
|
|
103
|
+
.fail(@failResponse)
|
|
104
|
+
|
|
105
|
+
all: (params) ->
|
|
106
|
+
@ajaxQueue(
|
|
107
|
+
params,
|
|
108
|
+
type: 'GET',
|
|
109
|
+
url: Ajax.getURL(@model)
|
|
110
|
+
).done(@recordsResponse)
|
|
111
|
+
.fail(@failResponse)
|
|
112
|
+
|
|
113
|
+
fetch: (params = {}, options = {}) ->
|
|
114
|
+
if id = params.id
|
|
115
|
+
delete params.id
|
|
116
|
+
@find(id, params).done (record) =>
|
|
117
|
+
@model.refresh(record, options)
|
|
118
|
+
else
|
|
119
|
+
@all(params).done (records) =>
|
|
120
|
+
@model.refresh(records, options)
|
|
121
|
+
|
|
122
|
+
# Private
|
|
123
|
+
|
|
124
|
+
recordsResponse: (data, status, xhr) =>
|
|
125
|
+
@model.trigger('ajaxSuccess', null, status, xhr)
|
|
126
|
+
|
|
127
|
+
failResponse: (xhr, statusText, error) =>
|
|
128
|
+
@model.trigger('ajaxError', null, xhr, statusText, error)
|
|
129
|
+
|
|
130
|
+
class Singleton extends Base
|
|
131
|
+
constructor: (@record) ->
|
|
132
|
+
@model = @record.constructor
|
|
133
|
+
|
|
134
|
+
reload: (params, options) ->
|
|
135
|
+
@ajaxQueue(
|
|
136
|
+
params,
|
|
137
|
+
type: 'GET'
|
|
138
|
+
url: Ajax.getURL(@record)
|
|
139
|
+
).done(@recordResponse(options))
|
|
140
|
+
.fail(@failResponse(options))
|
|
141
|
+
|
|
142
|
+
create: (params, options) ->
|
|
143
|
+
@ajaxQueue(
|
|
144
|
+
params,
|
|
145
|
+
type: 'POST'
|
|
146
|
+
contentType: 'application/json'
|
|
147
|
+
data: JSON.stringify(@record)
|
|
148
|
+
url: Ajax.getURL(@model)
|
|
149
|
+
).done(@recordResponse(options))
|
|
150
|
+
.fail(@failResponse(options))
|
|
151
|
+
|
|
152
|
+
update: (params, options) ->
|
|
153
|
+
@ajaxQueue(
|
|
154
|
+
params,
|
|
155
|
+
type: 'PUT'
|
|
156
|
+
contentType: 'application/json'
|
|
157
|
+
data: JSON.stringify(@record)
|
|
158
|
+
url: Ajax.getURL(@record)
|
|
159
|
+
).done(@recordResponse(options))
|
|
160
|
+
.fail(@failResponse(options))
|
|
161
|
+
|
|
162
|
+
destroy: (params, options) ->
|
|
163
|
+
@ajaxQueue(
|
|
164
|
+
params,
|
|
165
|
+
type: 'DELETE'
|
|
166
|
+
url: Ajax.getURL(@record)
|
|
167
|
+
).done(@recordResponse(options))
|
|
168
|
+
.fail(@failResponse(options))
|
|
169
|
+
|
|
170
|
+
# Private
|
|
171
|
+
|
|
172
|
+
recordResponse: (options = {}) =>
|
|
173
|
+
(data, status, xhr) =>
|
|
174
|
+
if Spine.isBlank(data) or @record.destroyed
|
|
175
|
+
data = false
|
|
176
|
+
else
|
|
177
|
+
data = @model.fromJSON(data)
|
|
178
|
+
|
|
179
|
+
Ajax.disable =>
|
|
180
|
+
if data
|
|
181
|
+
# ID change, need to do some shifting
|
|
182
|
+
if data.id and @record.id isnt data.id
|
|
183
|
+
@record.changeID(data.id)
|
|
184
|
+
# Update with latest data
|
|
185
|
+
@record.updateAttributes(data.attributes())
|
|
186
|
+
|
|
187
|
+
@record.trigger('ajaxSuccess', data, status, xhr)
|
|
188
|
+
options.success?.apply(@record) # Deprecated
|
|
189
|
+
options.done?.apply(@record)
|
|
190
|
+
|
|
191
|
+
failResponse: (options = {}) =>
|
|
192
|
+
(xhr, statusText, error) =>
|
|
193
|
+
@record.trigger('ajaxError', xhr, statusText, error)
|
|
194
|
+
options.error?.apply(@record) # Deprecated
|
|
195
|
+
options.fail?.apply(@record)
|
|
196
|
+
|
|
197
|
+
# Ajax endpoint
|
|
198
|
+
Model.host = ''
|
|
199
|
+
|
|
200
|
+
Include =
|
|
201
|
+
ajax: -> new Singleton(this)
|
|
202
|
+
|
|
203
|
+
url: (args...) ->
|
|
204
|
+
args.unshift(encodeURIComponent(@id))
|
|
205
|
+
Ajax.generateURL(@, args...)
|
|
206
|
+
|
|
207
|
+
Extend =
|
|
208
|
+
ajax: -> new Collection(this)
|
|
209
|
+
|
|
210
|
+
url: (args...) ->
|
|
211
|
+
Ajax.generateURL(@, args...)
|
|
212
|
+
|
|
213
|
+
Model.Ajax =
|
|
214
|
+
extended: ->
|
|
215
|
+
@fetch @ajaxFetch
|
|
216
|
+
@change @ajaxChange
|
|
217
|
+
@extend Extend
|
|
218
|
+
@include Include
|
|
219
|
+
|
|
220
|
+
# Private
|
|
221
|
+
|
|
222
|
+
ajaxFetch: ->
|
|
223
|
+
@ajax().fetch(arguments...)
|
|
224
|
+
|
|
225
|
+
ajaxChange: (record, type, options = {}) ->
|
|
226
|
+
return if options.ajax is false
|
|
227
|
+
record.ajax()[type](options.ajax, options)
|
|
228
|
+
|
|
229
|
+
Model.Ajax.Methods =
|
|
230
|
+
extended: ->
|
|
231
|
+
@extend Extend
|
|
232
|
+
@include Include
|
|
233
|
+
|
|
234
|
+
# Globals
|
|
235
|
+
Ajax.defaults = Base::defaults
|
|
236
|
+
Spine.Ajax = Ajax
|
|
237
|
+
module?.exports = Ajax
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
Spine = @Spine or require('spine')
|
|
2
|
+
$ = Spine.$
|
|
3
|
+
|
|
4
|
+
class Spine.List extends Spine.Controller
|
|
5
|
+
events:
|
|
6
|
+
'click .item': 'click'
|
|
7
|
+
|
|
8
|
+
selectFirst: false
|
|
9
|
+
|
|
10
|
+
constructor: ->
|
|
11
|
+
super
|
|
12
|
+
@bind 'change', @change
|
|
13
|
+
|
|
14
|
+
template: ->
|
|
15
|
+
throw 'Override template'
|
|
16
|
+
|
|
17
|
+
change: (item) =>
|
|
18
|
+
@current = item
|
|
19
|
+
|
|
20
|
+
unless @current
|
|
21
|
+
@children().removeClass('active')
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
@children().removeClass('active')
|
|
25
|
+
$(@children().get(@items.indexOf(@current))).addClass('active')
|
|
26
|
+
|
|
27
|
+
render: (items) ->
|
|
28
|
+
@items = items if items
|
|
29
|
+
@html @template(@items)
|
|
30
|
+
@change @current
|
|
31
|
+
if @selectFirst
|
|
32
|
+
unless @children('.active').length
|
|
33
|
+
@children(':first').click()
|
|
34
|
+
|
|
35
|
+
children: (sel) ->
|
|
36
|
+
@el.children(sel)
|
|
37
|
+
|
|
38
|
+
click: (e) ->
|
|
39
|
+
item = @items[$(e.currentTarget).index()]
|
|
40
|
+
@trigger('change', item)
|
|
41
|
+
true
|
|
42
|
+
|
|
43
|
+
module?.exports = Spine.List
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Spine = @Spine or require('spine')
|
|
2
|
+
|
|
3
|
+
Spine.Model.Local =
|
|
4
|
+
extended: ->
|
|
5
|
+
@change @saveLocal
|
|
6
|
+
@fetch @loadLocal
|
|
7
|
+
|
|
8
|
+
saveLocal: ->
|
|
9
|
+
result = JSON.stringify(@)
|
|
10
|
+
localStorage[@className] = result
|
|
11
|
+
|
|
12
|
+
loadLocal: ->
|
|
13
|
+
result = localStorage[@className]
|
|
14
|
+
@refresh(result or [], clear: true)
|
|
15
|
+
|
|
16
|
+
module?.exports = Spine.Model.Local
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
Spine = @Spine or require('spine')
|
|
2
|
+
$ = Spine.$
|
|
3
|
+
|
|
4
|
+
class Spine.Manager extends Spine.Module
|
|
5
|
+
@include Spine.Events
|
|
6
|
+
|
|
7
|
+
constructor: ->
|
|
8
|
+
@controllers = []
|
|
9
|
+
@bind 'change', @change
|
|
10
|
+
@add(arguments...)
|
|
11
|
+
|
|
12
|
+
add: (controllers...) ->
|
|
13
|
+
@addOne(cont) for cont in controllers
|
|
14
|
+
|
|
15
|
+
addOne: (controller) ->
|
|
16
|
+
controller.bind 'active', (args...) =>
|
|
17
|
+
@trigger('change', controller, args...)
|
|
18
|
+
controller.bind 'release', =>
|
|
19
|
+
@controllers.splice(@controllers.indexOf(controller), 1)
|
|
20
|
+
|
|
21
|
+
@controllers.push(controller)
|
|
22
|
+
|
|
23
|
+
deactivate: ->
|
|
24
|
+
@trigger('change', false, arguments...)
|
|
25
|
+
|
|
26
|
+
# Private
|
|
27
|
+
|
|
28
|
+
change: (current, args...) ->
|
|
29
|
+
for cont in @controllers
|
|
30
|
+
if cont is current
|
|
31
|
+
cont.activate(args...)
|
|
32
|
+
else
|
|
33
|
+
cont.deactivate(args...)
|
|
34
|
+
|
|
35
|
+
Spine.Controller.include
|
|
36
|
+
active: (args...) ->
|
|
37
|
+
if typeof args[0] is 'function'
|
|
38
|
+
@bind('active', args[0])
|
|
39
|
+
else
|
|
40
|
+
args.unshift('active')
|
|
41
|
+
@trigger(args...)
|
|
42
|
+
@
|
|
43
|
+
|
|
44
|
+
isActive: ->
|
|
45
|
+
@el.hasClass('active')
|
|
46
|
+
|
|
47
|
+
activate: ->
|
|
48
|
+
@el.addClass('active')
|
|
49
|
+
@
|
|
50
|
+
|
|
51
|
+
deactivate: ->
|
|
52
|
+
@el.removeClass('active')
|
|
53
|
+
@
|
|
54
|
+
|
|
55
|
+
class Spine.Stack extends Spine.Controller
|
|
56
|
+
controllers: {}
|
|
57
|
+
routes: {}
|
|
58
|
+
|
|
59
|
+
className: 'spine stack'
|
|
60
|
+
|
|
61
|
+
constructor: ->
|
|
62
|
+
super
|
|
63
|
+
|
|
64
|
+
@manager = new Spine.Manager
|
|
65
|
+
|
|
66
|
+
for key, value of @controllers
|
|
67
|
+
throw Error "'@#{ key }' already assigned - choose a different name" if @[key]?
|
|
68
|
+
@[key] = new value(stack: @)
|
|
69
|
+
@add(@[key])
|
|
70
|
+
|
|
71
|
+
for key, value of @routes
|
|
72
|
+
do (key, value) =>
|
|
73
|
+
callback = value if typeof value is 'function'
|
|
74
|
+
callback or= => @[value].active(arguments...)
|
|
75
|
+
@route(key, callback)
|
|
76
|
+
|
|
77
|
+
@[@default].active() if @default
|
|
78
|
+
|
|
79
|
+
add: (controller) ->
|
|
80
|
+
@manager.add(controller)
|
|
81
|
+
@append(controller)
|
|
82
|
+
|
|
83
|
+
module?.exports = Spine.Manager
|
|
84
|
+
module?.exports.Stack = Spine.Stack
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Spine = @Spine or require('spine')
|
|
2
|
+
isArray = Spine.isArray
|
|
3
|
+
require = @require or ((value) -> eval(value))
|
|
4
|
+
|
|
5
|
+
class Collection extends Spine.Module
|
|
6
|
+
constructor: (options = {}) ->
|
|
7
|
+
for key, value of options
|
|
8
|
+
@[key] = value
|
|
9
|
+
|
|
10
|
+
all: ->
|
|
11
|
+
@model.select (rec) => @associated(rec)
|
|
12
|
+
|
|
13
|
+
first: ->
|
|
14
|
+
@all()[0]
|
|
15
|
+
|
|
16
|
+
last: ->
|
|
17
|
+
values = @all()
|
|
18
|
+
values[values.length - 1]
|
|
19
|
+
|
|
20
|
+
count: ->
|
|
21
|
+
@all().length
|
|
22
|
+
|
|
23
|
+
find: (id) ->
|
|
24
|
+
records = @select (rec) =>
|
|
25
|
+
"#{rec.id}" is "#{id}"
|
|
26
|
+
throw new Error("\"#{@model.className}\" model could not find a record for the ID \"#{id}\"") unless records[0]
|
|
27
|
+
records[0]
|
|
28
|
+
|
|
29
|
+
findAllByAttribute: (name, value) ->
|
|
30
|
+
@model.select (rec) =>
|
|
31
|
+
@associated(rec) and rec[name] is value
|
|
32
|
+
|
|
33
|
+
findByAttribute: (name, value) ->
|
|
34
|
+
@findAllByAttribute(name, value)[0]
|
|
35
|
+
|
|
36
|
+
select: (cb) ->
|
|
37
|
+
@model.select (rec) =>
|
|
38
|
+
@associated(rec) and cb(rec)
|
|
39
|
+
|
|
40
|
+
refresh: (values) ->
|
|
41
|
+
return this unless values?
|
|
42
|
+
for record in @all()
|
|
43
|
+
delete @model.irecords[record.id]
|
|
44
|
+
for match, i in @model.records when match.id is record.id
|
|
45
|
+
@model.records.splice(i, 1)
|
|
46
|
+
break
|
|
47
|
+
values = [values] unless isArray(values)
|
|
48
|
+
for record in values
|
|
49
|
+
record.newRecord = false
|
|
50
|
+
record[@fkey] = @record.id
|
|
51
|
+
@model.refresh values
|
|
52
|
+
this
|
|
53
|
+
|
|
54
|
+
create: (record, options) ->
|
|
55
|
+
record[@fkey] = @record.id
|
|
56
|
+
@model.create(record, options)
|
|
57
|
+
|
|
58
|
+
add: (record, options) ->
|
|
59
|
+
record.updateAttribute @fkey, @record.id, options
|
|
60
|
+
|
|
61
|
+
remove: (record, options) ->
|
|
62
|
+
record.updateAttribute @fkey, null, options
|
|
63
|
+
|
|
64
|
+
# Private
|
|
65
|
+
|
|
66
|
+
associated: (record) ->
|
|
67
|
+
record[@fkey] is @record.id
|
|
68
|
+
|
|
69
|
+
class Instance extends Spine.Module
|
|
70
|
+
constructor: (options = {}) ->
|
|
71
|
+
for key, value of options
|
|
72
|
+
@[key] = value
|
|
73
|
+
|
|
74
|
+
exists: ->
|
|
75
|
+
return if @record[@fkey] then @model.exists(@record[@fkey]) else false
|
|
76
|
+
|
|
77
|
+
update: (value) ->
|
|
78
|
+
return this unless value?
|
|
79
|
+
unless value instanceof @model
|
|
80
|
+
value = new @model(value)
|
|
81
|
+
value.save() if value.isNew()
|
|
82
|
+
@record[@fkey] = value and value.id
|
|
83
|
+
this
|
|
84
|
+
|
|
85
|
+
class Singleton extends Spine.Module
|
|
86
|
+
constructor: (options = {}) ->
|
|
87
|
+
for key, value of options
|
|
88
|
+
@[key] = value
|
|
89
|
+
|
|
90
|
+
find: ->
|
|
91
|
+
@record.id and @model.findByAttribute(@fkey, @record.id)
|
|
92
|
+
|
|
93
|
+
update: (value) ->
|
|
94
|
+
return this unless value?
|
|
95
|
+
unless value instanceof @model
|
|
96
|
+
value = @model.fromJSON(value)
|
|
97
|
+
|
|
98
|
+
value[@fkey] = @record.id
|
|
99
|
+
value.save()
|
|
100
|
+
this
|
|
101
|
+
|
|
102
|
+
singularize = (str) ->
|
|
103
|
+
str.replace(/s$/, '')
|
|
104
|
+
|
|
105
|
+
underscore = (str) ->
|
|
106
|
+
str.replace(/::/g, '/')
|
|
107
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
|
|
108
|
+
.replace(/([a-z\d])([A-Z])/g, '$1_$2')
|
|
109
|
+
.replace(/-/g, '_')
|
|
110
|
+
.toLowerCase()
|
|
111
|
+
|
|
112
|
+
association = (name, model, record, fkey, Ctor) ->
|
|
113
|
+
model = require(model) if typeof model is 'string'
|
|
114
|
+
new Ctor(name: name, model: model, record: record, fkey: fkey)
|
|
115
|
+
|
|
116
|
+
Spine.Model.extend
|
|
117
|
+
hasMany: (name, model, fkey) ->
|
|
118
|
+
fkey ?= "#{underscore(this.className)}_id"
|
|
119
|
+
@::[name] = (value) ->
|
|
120
|
+
association(name, model, @, fkey, Collection).refresh(value)
|
|
121
|
+
|
|
122
|
+
belongsTo: (name, model, fkey) ->
|
|
123
|
+
fkey ?= "#{underscore(singularize(name))}_id"
|
|
124
|
+
@::[name] = (value) ->
|
|
125
|
+
association(name, model, @, fkey, Instance).update(value).exists()
|
|
126
|
+
|
|
127
|
+
@attributes.push(fkey)
|
|
128
|
+
|
|
129
|
+
hasOne: (name, model, fkey) ->
|
|
130
|
+
fkey ?= "#{underscore(@className)}_id"
|
|
131
|
+
@::[name] = (value) ->
|
|
132
|
+
association(name, model, @, fkey, Singleton).update(value).find()
|
|
133
|
+
|
|
134
|
+
Spine.Collection = Collection
|
|
135
|
+
Spine.Singleton = Singleton
|
|
136
|
+
Spine.Instance = Instance
|