-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
313 lines (263 loc) · 9.84 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
dotenv = require('dotenv').config()
const ch = require('clubhouse-lib')
// API Clients per workspace
const sourceApi = ch.create(process.env.CLUBHOUSE_API_TOKEN_SOURCE);
const targetApi = ch.create(process.env.CLUBHOUSE_API_TOKEN_TARGET);
const defaultSettings = {
// TODO: move to args
SOURCE_PROJECT_ID: 102,
TARGET_PROJECT_ID: 423,
TARGET_EPIC_ID: 422,
}
// Used to update story names that have been migrated from the source workspace
// and identify stories that have previously been migrated.
const migratedPrefix = '[Migrated:'
const addStoryLinks = async (settings) => {
const sourceProjectId = settings.source_project || defaultSettings.SOURCE_PROJECT_ID
// Handle mapping for story links (x blocks y, etc)
// This should run AFTER stories have been migrated.
let storiesMap = {}
let allStoryLinks = []
let sourceStories = await sourceApi.listStories(sourceProjectId).
then(stories => {
stories.forEach(s => {
s.story_links.forEach(link => {
allStoryLinks.push({
archived: s.archived,
story_to_fix: s.id,
old_subject_id: link.subject_id,
verb: link.verb,
old_object_id: link.object_id,
created_at: link.created_id,
updated_at: link.updated_at,
})
})
// parse out the new id from the old story name, add to the map.
const newId = s.name.split(migratedPrefix).pop().split(']')[0]
storiesMap[s.id] = newId
})
return stories
})
console.log(`Creating missing story links for ${allStoryLinks.length} stories`)
for (let link of allStoryLinks) {
let linkParam = {
object_id: storiesMap[link.old_object_id],
subject_id: storiesMap[link.old_subject_id],
verb: link.verb,
}
console.log(linkParam.subject_id, linkParam.verb, linkParam.object_id)
try {
await targetApi.createStoryLink(linkParam).then(console.log)
} catch(err) {
// Likely already imported.
// console.log(err)
}
}
}
const createIterationsFromSource = async (unusedSettings) => {
const existingTargetIters = await targetApi.listIterations().then(iters => {
return iters.map(iter => iter.name)
})
await sourceApi.listIterations().then(iters => {
iters.map(async iter => {
if (!existingTargetIters.includes(iter.name)) {
const importIter = {
name: iter.name,
start_date: iter.start_date,
end_date: iter.end_date,
}
await targetApi.createIteration(importIter).then(console.log)
}
})
})
}
const importOne = async (settings) => {
const storyId = settings.story
const targetProjectId = settings.target_project || defaultSettings.TARGET_PROJECT_ID
const targetEpicId = settings.target_epic || defaultSettings.TARGET_EPIC_ID
const resourceMaps = await getResourceMaps()
let newStory = await getStoryForImport(
storyId, resourceMaps, targetProjectId, targetEpicId)
await updateStory(newStory)
}
const importAll = async (settings) => {
const sourceProjectId = settings.source_project || defaultSettings.SOURCE_PROJECT_ID
const targetProjectId = settings.target_project || defaultSettings.TARGET_PROJECT_ID
const targetEpicId = settings.target_epic || defaultSettings.TARGET_EPIC_ID
await sourceApi.listProjects().then(projs => {
projs.forEach(p => console.log(p.name))
})
const sourceStoryIds = await sourceApi.listStories(sourceProjectId)
.then(stories => {
return stories.map(s => s.id)
})
console.log(sourceStoryIds)
const resourceMaps = await getResourceMaps()
let toImport = []
for (let storyId of sourceStoryIds) {
let newStory = await getStoryForImport(
storyId, resourceMaps, targetProjectId, targetEpicId)
toImport.push(newStory)
}
//toImport = toImport.slice(0, 10)
console.log(toImport.length)
for (let newStory of toImport) {
await updateStory(newStory)
}
}
const updateStory = async (newStory) => {
// console.log(newStory)
if (!newStory.create.name.startsWith(migratedPrefix)) {
await targetApi.createStory(newStory.create).then(async res => {
console.log(`Created new story #${res.id}: ${res.name}`)
console.log(` - - via old source story #${newStory.id}`)
const origDescription = newStory.create.description || ''
let updateSource = {
name: `${migratedPrefix}${res.id}] ${newStory.create.name}`,
description: `${origDescription}\n\n** Migrated to ${res.app_url} **`,
}
await sourceApi.updateStory(newStory.id, updateSource).then(console.log)
})
} else {
console.log(`....We have already migrated this story... ~ ${newStory.create.name}`)
}
}
const getStoryForImport = async (storyId, resourceMaps, projectId, epicId) => {
const members = resourceMaps.members
const iterations = resourceMaps.iterations
const workflows = resourceMaps.workflows
const s = await sourceApi.getStory(storyId).then(sty => {
console.log(`Fetched source story #${sty.id} - ${sty.name}`)
return sty
})
let sourceComments = s.comments.map(c => {
return {
author_id: members[c.author_id],
created_at: c.created_at,
updated_at: c.updated_at,
text: c.text,
}
})
let sourceTasks = s.tasks.map(t => {
return {
// a task is "complete" not "completed" like stories.
complete: t.complete,
owner_ids: mapMembers(t.owner_ids, members),
created_at: t.created_at,
updated_at: t.updated_at,
description: t.description,
}
})
let newStory = {
archived: s.archived,
comments: sourceComments,
completed_at_override: s.created_at_override,
created_at: s.created_at,
deadline: s.deadline,
description: s.description,
epic_id: epicId,
estimate: s.estimate,
external_id: s.app_url,
follower_ids: mapMembers(s.follower_ids, members),
iteration_id: iterations[s.iteration_id],
name: s.name,
owner_ids: mapMembers(s.owner_ids, members),
project_id: projectId,
requested_by_id: members[s.requested_by_id],
started_at_override: s.started_at_override,
story_type: s.story_type,
tasks: sourceTasks,
updated_at: s.updated_at,
workflow_state_id: workflows[s.workflow_state_id],
}
return {
id: s.id,
create: _cleanObj(newStory)
}
}
const mapMembers = (oldMemberIds, membersMap) => {
const memberIds = []
oldMemberIds.forEach(o_id => {
const newId = membersMap[o_id]
if (newId) {
memberIds.push(newId)
}
})
return memberIds
}
const _getMapObj = async (apiCall, keyField, innerArrayField) => {
const sourceMapNameToId = {}
await sourceApi[apiCall]().then(list => {
list.forEach(i => {
if (innerArrayField) {
i[innerArrayField].forEach(inner => {
sourceMapNameToId[_resolve(keyField, inner)] = inner.id
})
} else {
sourceMapNameToId[_resolve(keyField, i)] = i.id
}
})
})
console.log(`...Temp map by ${keyField} for ${apiCall}`)
console.log(sourceMapNameToId)
const mapSourceToTargetIds = {}
await targetApi[apiCall]().then(list => {
list.forEach(i => {
if (innerArrayField) {
i[innerArrayField].forEach(inner => {
const oldId = sourceMapNameToId[_resolve(keyField, inner)]
if (oldId) {
mapSourceToTargetIds[oldId] = inner.id
}
})
} else {
const oldId = sourceMapNameToId[_resolve(keyField, i)]
if (oldId) {
mapSourceToTargetIds[oldId] = i.id
}
}
})
})
console.log(`...ID map for ${apiCall}`)
console.log(mapSourceToTargetIds)
return mapSourceToTargetIds
}
/* Create objects mapping old workspace ids to new workspace ids for
member, iterataion, and workflow resources
TODO: do this for epics too.
*/
const getResourceMaps = async () => {
const membersMap = await _getMapObj('listMembers', 'profile.email_address')
const itersMap = await _getMapObj('listIterations', 'name')
const wfMap = await _getMapObj('listWorkflows', 'name', 'states')
return {
members: membersMap,
iterations: itersMap,
workflows: wfMap,
}
}
/* Utility to remove null and undefined values from an object */
const _cleanObj = (obj) => {
var propNames = Object.getOwnPropertyNames(obj);
for (var i = 0; i < propNames.length; i++) {
var propName = propNames[i];
if (obj[propName] === null || obj[propName] === undefined) {
delete obj[propName];
}
}
return obj
}
/* Utility to do a deep resolution of a nested object key */
const _resolve = (path, obj=self, separator='.') => {
var properties = Array.isArray(path) ? path : path.split(separator)
return properties.reduce((prev, curr) => prev && prev[curr], obj)
}
module.exports = {
importAll: importAll,
importOne: importOne,
linkStories: addStoryLinks,
addIterations: createIterationsFromSource,
}
require('make-runnable/custom')({
printOutputFrame: false
})