Skip to content

Commit 2054ebf

Browse files
mifikvz
andauthored
Generate assemblyId on client (#106)
* Generate assemblyId on client And allow user to retrieve it by calling promise.assemblyId This allows the user to use or log the assemblyId even before it has been created for easier debugging * Update README.md Co-authored-by: Kevin van Zonneveld <[email protected]>
1 parent 384bb4e commit 2054ebf

File tree

6 files changed

+125
-81
lines changed

6 files changed

+125
-81
lines changed

Diff for: README.md

+9
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,15 @@ function onAssemblyProgress(assembly) {
190190
}
191191
```
192192

193+
**Tip:** `createAssembly` returns a `Promise` with an extra property `assemblyId`. This can be used to retrieve the Assembly ID before the Assembly has even been created. Useful for debugging by logging this ID when the request starts, for example:
194+
195+
```js
196+
const promise = transloadit.createAssembly(options)
197+
console.log('Creating', promise.assemblyId)
198+
const status = await promise
199+
```
200+
201+
193202
See also:
194203
- [API documentation](https://transloadit.com/docs/api/#assemblies-post)
195204
- Error codes and retry logic below

Diff for: package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"is-stream": "^2.0.0",
2323
"lodash": "^4.17.20",
2424
"p-map": "^4.0.0",
25-
"tus-js-client": "^2.2.0"
25+
"tus-js-client": "^2.2.0",
26+
"uuid": "^8.3.2"
2627
},
2728
"devDependencies": {
2829
"@babel/eslint-plugin": "^7.13.10",
@@ -42,8 +43,7 @@
4243
"p-retry": "^4.2.0",
4344
"request": "^2.88.2",
4445
"temp": "^0.9.1",
45-
"tsd": "^0.14.0",
46-
"uuid": "^8.3.2"
46+
"tsd": "^0.14.0"
4747
},
4848
"repository": {
4949
"type": "git",

Diff for: src/Transloadit.js

+80-65
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const intoStream = require('into-stream')
1313
const isStream = require('is-stream')
1414
const assert = require('assert')
1515
const pMap = require('p-map')
16+
const uuid = require('uuid')
1617

1718
const PaginationStream = require('./PaginationStream')
1819
const { version } = require('../package.json')
@@ -173,7 +174,7 @@ class TransloaditClient {
173174
* @param {object} opts assembly options
174175
* @returns {Promise}
175176
*/
176-
async createAssembly (opts = {}, arg2) {
177+
createAssembly (opts = {}, arg2) {
177178
// Warn users of old callback API
178179
if (typeof arg2 === 'function') {
179180
throw new TypeError('You are trying to send a function as the second argument. This is no longer valid in this version. Please see github README for usage.')
@@ -194,90 +195,104 @@ class TransloaditClient {
194195
// Keep track of how long the request took
195196
const startTimeMs = getHrTimeMs()
196197

197-
// Undocumented feature to allow specifying the assembly id from the client
198-
// (not recommended for general use due to security)
199-
const urlSuffix = assemblyId != null ? `/assemblies/${assemblyId}` : '/assemblies'
198+
// Undocumented feature to allow specifying a custom assembly id from the client
199+
// Not recommended for general use due to security. E.g if the user doesn't provide a cryptographically
200+
// secure ID, then anyone could access the assembly.
201+
let effectiveAssemblyId
202+
if (assemblyId != null) {
203+
effectiveAssemblyId = assemblyId
204+
} else {
205+
effectiveAssemblyId = uuid.v4().replace(/-/g, '')
206+
}
207+
const urlSuffix = `/assemblies/${effectiveAssemblyId}`
200208

201-
this._lastUsedAssemblyUrl = `${this._endpoint}${urlSuffix}`
209+
// We want to be able to return the promise immediately with custom data
210+
const promise = (async () => {
211+
this._lastUsedAssemblyUrl = `${this._endpoint}${urlSuffix}`
202212

203-
// eslint-disable-next-line no-bitwise
204-
await pMap(Object.entries(files), async ([, path]) => access(path, fs.F_OK | fs.R_OK), { concurrency: 5 })
213+
// eslint-disable-next-line no-bitwise
214+
await pMap(Object.entries(files), async ([, path]) => access(path, fs.F_OK | fs.R_OK), { concurrency: 5 })
205215

206-
// Convert uploads to streams
207-
const streamsMap = fromPairs(Object.entries(uploads).map(([label, value]) => {
208-
const isReadable = isStream.readable(value)
209-
if (!isReadable && isStream(value)) {
210-
// https://github.com/transloadit/node-sdk/issues/92
211-
throw new Error(`Upload named "${label}" is not a Readable stream`)
212-
}
216+
// Convert uploads to streams
217+
const streamsMap = fromPairs(Object.entries(uploads).map(([label, value]) => {
218+
const isReadable = isStream.readable(value)
219+
if (!isReadable && isStream(value)) {
220+
// https://github.com/transloadit/node-sdk/issues/92
221+
throw new Error(`Upload named "${label}" is not a Readable stream`)
222+
}
213223

214-
return [
215-
label,
216-
isStream.readable(value) ? value : intoStream(value),
217-
]
218-
}))
224+
return [
225+
label,
226+
isStream.readable(value) ? value : intoStream(value),
227+
]
228+
}))
219229

220-
// Wrap in object structure (so we can know if it's a pathless stream or not)
221-
const allStreamsMap = fromPairs(Object.entries(streamsMap).map(([label, stream]) => [label, { stream }]))
230+
// Wrap in object structure (so we can know if it's a pathless stream or not)
231+
const allStreamsMap = fromPairs(Object.entries(streamsMap).map(([label, stream]) => [label, { stream }]))
222232

223-
// Create streams from files too
224-
for (const [label, path] of Object.entries(files)) {
225-
const stream = fs.createReadStream(path)
226-
allStreamsMap[label] = { stream, path } // File streams have path
227-
}
233+
// Create streams from files too
234+
for (const [label, path] of Object.entries(files)) {
235+
const stream = fs.createReadStream(path)
236+
allStreamsMap[label] = { stream, path } // File streams have path
237+
}
228238

229-
const allStreams = Object.values(allStreamsMap)
239+
const allStreams = Object.values(allStreamsMap)
230240

231-
// Pause all streams
232-
allStreams.forEach(({ stream }) => stream.pause())
241+
// Pause all streams
242+
allStreams.forEach(({ stream }) => stream.pause())
233243

234-
// If any stream emits error, we want to handle this and exit with error
235-
const streamErrorPromise = new Promise((resolve, reject) => {
236-
allStreams.forEach(({ stream }) => stream.on('error', reject))
237-
})
244+
// If any stream emits error, we want to handle this and exit with error
245+
const streamErrorPromise = new Promise((resolve, reject) => {
246+
allStreams.forEach(({ stream }) => stream.on('error', reject))
247+
})
238248

239-
const createAssemblyAndUpload = async () => {
240-
const useTus = isResumable && allStreams.every(isFileBasedStream)
249+
const createAssemblyAndUpload = async () => {
250+
const useTus = isResumable && allStreams.every(isFileBasedStream)
241251

242-
const requestOpts = {
243-
urlSuffix,
244-
method: 'post',
245-
timeout,
246-
params,
247-
}
252+
const requestOpts = {
253+
urlSuffix,
254+
method: 'post',
255+
timeout,
256+
params,
257+
}
248258

249-
if (useTus) {
250-
requestOpts.fields = {
251-
tus_num_expected_upload_files: allStreams.length,
259+
if (useTus) {
260+
requestOpts.fields = {
261+
tus_num_expected_upload_files: allStreams.length,
262+
}
263+
} else if (isResumable) {
264+
logWarn('Disabling resumability because the size of one or more streams cannot be determined')
252265
}
253-
} else if (isResumable) {
254-
logWarn('Disabling resumability because the size of one or more streams cannot be determined')
255-
}
256266

257-
// upload as form multipart or tus?
258-
const formUploadStreamsMap = useTus ? {} : allStreamsMap
259-
const tusStreamsMap = useTus ? allStreamsMap : {}
267+
// upload as form multipart or tus?
268+
const formUploadStreamsMap = useTus ? {} : allStreamsMap
269+
const tusStreamsMap = useTus ? allStreamsMap : {}
260270

261-
const result = await this._remoteJson(requestOpts, formUploadStreamsMap, onUploadProgress)
262-
checkResult(result)
271+
const result = await this._remoteJson(requestOpts, formUploadStreamsMap, onUploadProgress)
272+
checkResult(result)
263273

264-
if (useTus && Object.keys(tusStreamsMap).length > 0) {
265-
await sendTusRequest({
266-
streamsMap: tusStreamsMap,
267-
assembly : result,
268-
onProgress: onUploadProgress,
274+
if (useTus && Object.keys(tusStreamsMap).length > 0) {
275+
await sendTusRequest({
276+
streamsMap: tusStreamsMap,
277+
assembly : result,
278+
onProgress: onUploadProgress,
279+
})
280+
}
281+
282+
if (!waitForCompletion) return result
283+
const awaitResult = await this.awaitAssemblyCompletion(result.assembly_id, {
284+
timeout, onAssemblyProgress, startTimeMs,
269285
})
286+
checkResult(awaitResult)
287+
return awaitResult
270288
}
271289

272-
if (!waitForCompletion) return result
273-
const awaitResult = await this.awaitAssemblyCompletion(result.assembly_id, {
274-
timeout, onAssemblyProgress, startTimeMs,
275-
})
276-
checkResult(awaitResult)
277-
return awaitResult
278-
}
290+
return Promise.race([createAssemblyAndUpload(), streamErrorPromise])
291+
})()
279292

280-
return Promise.race([createAssemblyAndUpload(), streamErrorPromise])
293+
// This allows the user to use or log the assemblyId even before it has been created for easier debugging
294+
promise.assemblyId = effectiveAssemblyId
295+
return promise
281296
}
282297

283298
async awaitAssemblyCompletion (assemblyId, {

Diff for: test/integration/__tests__/live-api.js

+22-4
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,11 @@ function createClient (opts = {}) {
3737
return new Transloadit({ authKey, authSecret, ...opts })
3838
}
3939

40-
async function createAssembly (client, params) {
41-
const assemblyId = uuid.v4().replace(/-/g, '')
42-
console.log('createAssembly', assemblyId)
43-
return client.createAssembly({ assemblyId, ...params })
40+
function createAssembly (client, params) {
41+
const promise = client.createAssembly(params)
42+
const { assemblyId } = promise
43+
console.log('createAssembly', assemblyId) // For easier debugging
44+
return promise
4445
}
4546

4647
const startServerAsync = async (handler) => new Promise((resolve, reject) => {
@@ -287,6 +288,23 @@ describe('API integration', () => {
287288
expect(result.assembly_id).toEqual(assemblyId)
288289
})
289290

291+
it('should allow getting the assemblyId on createAssembly even before it has been started', async () => {
292+
const client = createClient()
293+
294+
const params = {
295+
params: {
296+
steps: {
297+
dummy: dummyStep,
298+
},
299+
},
300+
}
301+
302+
const promise = createAssembly(client, params)
303+
expect(promise.assemblyId).toMatch(/^[\da-f]+$/)
304+
const result = await promise
305+
expect(result.assembly_id).toMatch(promise.assemblyId)
306+
})
307+
290308
it('should throw a proper error for request stream', async () => {
291309
const client = createClient()
292310

Diff for: test/unit/__tests__/mock-http.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ jest.setTimeout(1000)
66

77
const getLocalClient = (opts) => new Transloadit({ authKey: '', authSecret: '', endpoint: 'http://localhost', ...opts })
88

9+
const createAssemblyRegex = /\/assemblies\/[0-9a-f]{32}/
10+
911
describe('Mocked API tests', () => {
1012
afterEach(() => {
1113
nock.cleanAll()
@@ -16,7 +18,7 @@ describe('Mocked API tests', () => {
1618
const client = new Transloadit({ authKey: '', authSecret: '', endpoint: 'http://localhost' })
1719

1820
nock('http://localhost')
19-
.post('/assemblies')
21+
.post(createAssemblyRegex)
2022
.delay(100)
2123
.reply(200)
2224

@@ -51,7 +53,7 @@ describe('Mocked API tests', () => {
5153
const client = getLocalClient()
5254

5355
const scope = nock('http://localhost')
54-
.post('/assemblies')
56+
.post(createAssemblyRegex)
5557
.reply(200, { ok: 'ASSEMBLY_UPLOADING' })
5658
.get('/assemblies/1')
5759
.query(() => true)
@@ -83,7 +85,7 @@ describe('Mocked API tests', () => {
8385
const client = getLocalClient()
8486

8587
nock('http://localhost')
86-
.post('/assemblies')
88+
.post(createAssemblyRegex)
8789
.reply(400, { error: 'INVALID_FILE_META_DATA' })
8890

8991
await expect(client.createAssembly()).rejects.toThrow(expect.objectContaining({ transloaditErrorCode: 'INVALID_FILE_META_DATA', message: 'INVALID_FILE_META_DATA' }))
@@ -93,7 +95,7 @@ describe('Mocked API tests', () => {
9395
const client = getLocalClient()
9496

9597
nock('http://localhost')
96-
.post('/assemblies')
98+
.post(createAssemblyRegex)
9799
.reply(400, { error: 'INVALID_FILE_META_DATA', assembly_id: '123', assembly_ssl_url: 'https://api2-oltu.transloadit.com/assemblies/foo' })
98100

99101
await expect(client.createAssembly()).rejects.toThrow(expect.objectContaining({
@@ -111,9 +113,9 @@ describe('Mocked API tests', () => {
111113
// https://transloadit.com/blog/2012/04/introducing-rate-limiting/
112114

113115
const scope = nock('http://localhost')
114-
.post('/assemblies')
116+
.post(createAssemblyRegex)
115117
.reply(413, { error: 'RATE_LIMIT_REACHED', info: { retryIn: 0.01 } })
116-
.post('/assemblies')
118+
.post(createAssemblyRegex)
117119
.reply(200, { ok: 'ASSEMBLY_EXECUTING' })
118120

119121
await client.createAssembly()
@@ -124,7 +126,7 @@ describe('Mocked API tests', () => {
124126
const client = getLocalClient({ maxRetries: 0 })
125127

126128
const scope = nock('http://localhost')
127-
.post('/assemblies')
129+
.post(createAssemblyRegex)
128130
.reply(413, { error: 'RATE_LIMIT_REACHED', message: 'Request limit reached', info: { retryIn: 0.01 } })
129131

130132
await expect(client.createAssembly()).rejects.toThrow(expect.objectContaining({ transloaditErrorCode: 'RATE_LIMIT_REACHED', message: 'RATE_LIMIT_REACHED: Request limit reached' }))
@@ -190,7 +192,7 @@ describe('Mocked API tests', () => {
190192
const client = getLocalClient()
191193

192194
const scope = nock('http://localhost')
193-
.post('/assemblies')
195+
.post(createAssemblyRegex)
194196
.reply(200, { error: 'IMPORT_FILE_ERROR', assembly_id: '1' })
195197

196198
await expect(client.createAssembly()).rejects.toThrow(expect.objectContaining({

Diff for: test/unit/__tests__/test-transloadit-client.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ describe('Transloadit', () => {
207207
it('should crash if attempt to use callback', async () => {
208208
const client = new Transloadit({ authKey: 'foo_key', authSecret: 'foo_secret' })
209209
const cb = () => {}
210-
await expect(client.createAssembly({}, cb)).rejects.toThrow(TypeError)
210+
expect(() => client.createAssembly({}, cb)).toThrow(TypeError)
211211
})
212212

213213
describe('_calcSignature', () => {

0 commit comments

Comments
 (0)