UNPKG

6.8 kBtext/coffeescriptView Raw
1'use strict'
2
3EntityPool = require './entity-pool'
4DomainError = require './lib/domain-error'
5Util = require './util'
6
7debug = require('debug')('base-domain:fixture-loader')
8
9###*
10Load fixture data (only works in Node.js)
11
12@class FixtureLoader
13@module base-domain
14###
15class FixtureLoader
16
17 constructor: (@facade, @fixtureDirs = []) ->
18 if not Array.isArray @fixtureDirs
19 @fixtureDirs = [ @fixtureDirs ]
20
21 @entityPool = new EntityPool
22 @fixturesByModel = {}
23
24
25 ###*
26 @method load
27 @public
28 @param {Object} [options]
29 @param {Boolean} [options.async] if true, returns Promise.
30 @return {EntityPool|Promise(EntityPool)}
31 ###
32 load: (options = {}) ->
33 { fs, requireFile } = @facade.constructor # only defined in Node.js
34
35 try
36
37 modelNames = []
38 for fixtureDir in @fixtureDirs
39
40 for file in fs.readdirSync fixtureDir + '/data'
41 [ modelName, ext ] = file.split('.')
42 continue if ext not in ['coffee', 'js', 'json']
43 path = fixtureDir + '/data/' + file
44 fx = requireFile(path)
45 fx.path = path
46 fx.fixtureDir = fixtureDir
47 @fixturesByModel[modelName] = fx
48 modelNames.push modelName
49
50 modelNames = @topoSort(modelNames)
51
52 names = options.names ? modelNames
53
54 modelNames = modelNames.filter (name) -> name in names
55
56 if options.async
57 return @saveAsync(modelNames).then => @entityPool
58
59 else
60 for modelName in modelNames
61 @loadAndSaveModels(modelName)
62
63 return @entityPool
64
65 catch e
66 if options.async then Promise.reject(e) else throw e
67
68
69 ###*
70 @private
71 ###
72 saveAsync: (modelNames) ->
73
74 if not modelNames.length
75 return Promise.resolve(true)
76
77 modelName = modelNames.shift()
78
79 Promise.resolve(@loadAndSaveModels(modelName)).then =>
80 @saveAsync(modelNames)
81
82
83 ###*
84 @private
85 ###
86 loadAndSaveModels: (modelName) ->
87
88 fx = @fixturesByModel[modelName]
89
90 data =
91 switch typeof fx.data
92 when 'string'
93 @readTSV(fx.fixtureDir, fx.data)
94 when 'function'
95 fx.data.call(new Scope(@, fx), @entityPool)
96 when 'object'
97 fx.data
98
99 try
100 repo = @facade.createPreferredRepository(modelName) # TODO: enable to add 2nd argument (noParent: boolean)
101 catch e
102 console.error e.message
103 return
104
105 if not data?
106 throw new Error("Invalid fixture in model '#{modelName}'. Check the fixture file: #{fx.path}")
107
108 ids = Object.keys(data)
109 debug('inserting %s models into %s', ids.length, modelName)
110
111 # save models portion by portion, considering parallel connection size
112 PORTION_SIZE = 5
113 do saveModelsByPortion = =>
114 return if ids.length is 0
115
116 idsPortion = ids.slice(0, PORTION_SIZE)
117 ids = ids.slice(idsPortion.length)
118
119 results = for id in idsPortion
120 obj = data[id]
121 obj.id = id
122 @saveModel(repo, obj)
123
124 if Util.isPromise results[0]
125 Promise.all(results).then =>
126 saveModelsByPortion()
127 else
128 saveModelsByPortion()
129
130
131
132 saveModel: (repo, obj) ->
133
134 result = repo.save obj,
135 method : 'create'
136 fixtureInsertion : true # save even if the repository is master
137 include:
138 entityPool: @entityPool
139
140 if Util.isPromise result
141 result.then (entity) =>
142 @entityPool.set entity
143
144 else
145 @entityPool.set result
146
147
148
149 ###*
150 topological sort
151
152 @method topoSort
153 @private
154 ###
155 topoSort: (names) ->
156
157 # adds dependent models
158 namesWithDependencies = []
159
160 for el in names
161 do add = (name = el) =>
162
163 return if name in namesWithDependencies
164
165 namesWithDependencies.push name
166
167 fx = @fixturesByModel[name]
168
169 unless fx?
170 throw new DomainError 'base-domain:modelNotFound',
171 "model '#{name}' is not found. It might be written in some 'dependencies' property."
172
173 add(depname) for depname in fx.dependencies ? []
174
175
176 # topological sort
177 visited = {}
178 sortedNames = []
179
180 for el in namesWithDependencies
181
182 do visit = (name = el, ancestors = []) =>
183
184 fx = @fixturesByModel[name]
185
186 return if visited[name]?
187
188 ancestors.push(name)
189
190 visited[name] = true
191
192 for depname in fx.dependencies ? []
193
194 if depname in ancestors
195 throw new DomainError 'base-domain:dependencyLoop',
196 'dependency chain is making loop'
197
198 visit(depname, ancestors.slice())
199
200 sortedNames.push(name)
201
202 return sortedNames
203
204
205 ###*
206 read TSV, returns model data
207
208 @method readTSV
209 @private
210 ###
211 readTSV: (fixtureDir, file) ->
212 { fs } = @facade.constructor # only defined in Node.js
213
214 objs = {}
215
216 lines = fs.readFileSync(fixtureDir + '/tsvs/' + file, 'utf8').split('\n')
217
218 tsv = (line.split('\t') for line in lines)
219
220 names = tsv.shift() # first line is title
221 names.shift() # first column is id
222
223 for data in tsv
224 obj = {}
225 id = data.shift()
226 obj.id = id
227
228 break if not id # omit reading all lines below the line whose id is empty
229
230 for name, i in names
231 break if not name # omit reading all columns at right side of the column whose title is empty
232 value = data[i]
233 value = Number(value) if value.match(/^[0-9]+$/) # regard number-like values as a number
234 obj[name] = value
235
236 objs[obj.id] = obj
237
238 return objs
239
240
241###*
242'this' property in fixture's data function
243
244this.readTSV('xxx.tsv') is available
245
246 module.exports = {
247 data: function(entityPool) {
248 this.readTSV('model-name.tsv');
249 }
250 };
251
252@class Scope
253@private
254###
255class Scope
256
257 constructor: (@loader, @fx) ->
258
259 ###*
260 @method readTSV
261 @param {String} filename filename (directory is automatically set)
262 @return {Object} tsv contents
263 ###
264 readTSV: (filename) ->
265 @loader.readTSV(@fx.fixtureDir, filename)
266
267
268module.exports = FixtureLoader