@toa.io/extensions.storages 1.0.0-alpha.13 → 1.0.0-alpha.143

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.
Files changed (90) hide show
  1. package/package.json +13 -14
  2. package/readme.md +94 -49
  3. package/schemas/annotation.cos.yaml +1 -0
  4. package/schemas/cloudinary.cos.yaml +30 -0
  5. package/schemas/fs.cos.yaml +2 -0
  6. package/schemas/s3.cos.yaml +1 -0
  7. package/schemas/tmp.cos.yaml +0 -1
  8. package/source/Entry.ts +11 -11
  9. package/source/Factory.ts +16 -9
  10. package/source/Provider.ts +19 -19
  11. package/source/Scanner.ts +57 -24
  12. package/source/Secrets.ts +6 -0
  13. package/source/Storage.test.ts +103 -279
  14. package/source/Storage.ts +55 -204
  15. package/source/deployment.ts +48 -15
  16. package/source/errors.ts +3 -0
  17. package/source/index.ts +1 -1
  18. package/source/manifest.ts +7 -6
  19. package/source/providers/Cloudinary.ts +274 -0
  20. package/source/providers/Declaration.ts +2 -1
  21. package/source/providers/FileSystem.ts +79 -25
  22. package/source/providers/S3.ts +81 -80
  23. package/source/providers/Temporary.ts +2 -3
  24. package/source/providers/Test.ts +4 -4
  25. package/source/providers/index.test.ts +102 -76
  26. package/source/providers/index.ts +9 -4
  27. package/source/schemas.ts +1 -0
  28. package/source/test/.env.example +4 -0
  29. package/source/test/arny.jpg +0 -0
  30. package/source/test/lenna.48x48.jpeg +0 -0
  31. package/source/test/sample.avi +0 -0
  32. package/source/test/sample.svg +4 -0
  33. package/source/test/sample.wav +0 -0
  34. package/source/test/util.ts +54 -12
  35. package/transpiled/Entry.d.ts +12 -10
  36. package/transpiled/Factory.js +11 -5
  37. package/transpiled/Factory.js.map +1 -1
  38. package/transpiled/Provider.d.ts +15 -15
  39. package/transpiled/Provider.js +3 -3
  40. package/transpiled/Provider.js.map +1 -1
  41. package/transpiled/Scanner.d.ts +5 -2
  42. package/transpiled/Scanner.js +49 -22
  43. package/transpiled/Scanner.js.map +1 -1
  44. package/transpiled/Secrets.d.ts +5 -0
  45. package/transpiled/Secrets.js +3 -0
  46. package/transpiled/Secrets.js.map +1 -0
  47. package/transpiled/Storage.d.ts +10 -19
  48. package/transpiled/Storage.js +47 -153
  49. package/transpiled/Storage.js.map +1 -1
  50. package/transpiled/deployment.d.ts +1 -1
  51. package/transpiled/deployment.js +40 -15
  52. package/transpiled/deployment.js.map +1 -1
  53. package/transpiled/errors.d.ts +2 -0
  54. package/transpiled/errors.js +6 -0
  55. package/transpiled/errors.js.map +1 -0
  56. package/transpiled/index.d.ts +1 -1
  57. package/transpiled/manifest.js +5 -2
  58. package/transpiled/manifest.js.map +1 -1
  59. package/transpiled/providers/Cloudinary.d.ts +42 -0
  60. package/transpiled/providers/Cloudinary.js +189 -0
  61. package/transpiled/providers/Cloudinary.js.map +1 -0
  62. package/transpiled/providers/Declaration.d.ts +3 -2
  63. package/transpiled/providers/FileSystem.d.ts +15 -7
  64. package/transpiled/providers/FileSystem.js +64 -24
  65. package/transpiled/providers/FileSystem.js.map +1 -1
  66. package/transpiled/providers/S3.d.ts +14 -14
  67. package/transpiled/providers/S3.js +62 -60
  68. package/transpiled/providers/S3.js.map +1 -1
  69. package/transpiled/providers/Temporary.d.ts +1 -2
  70. package/transpiled/providers/Temporary.js +2 -2
  71. package/transpiled/providers/Temporary.js.map +1 -1
  72. package/transpiled/providers/Test.d.ts +3 -3
  73. package/transpiled/providers/Test.js +2 -2
  74. package/transpiled/providers/Test.js.map +1 -1
  75. package/transpiled/providers/index.d.ts +6 -2
  76. package/transpiled/providers/index.js +2 -2
  77. package/transpiled/providers/index.js.map +1 -1
  78. package/transpiled/schemas.d.ts +1 -0
  79. package/transpiled/schemas.js +2 -1
  80. package/transpiled/schemas.js.map +1 -1
  81. package/transpiled/test/util.d.ts +70 -5
  82. package/transpiled/test/util.js +43 -4
  83. package/transpiled/test/util.js.map +1 -1
  84. package/transpiled/tsconfig.tsbuildinfo +1 -1
  85. package/source/providers/FileSystem.test.ts +0 -5
  86. package/source/providers/Memory.ts +0 -41
  87. package/source/providers/S3.test.ts +0 -133
  88. package/transpiled/providers/Memory.d.ts +0 -13
  89. package/transpiled/providers/Memory.js +0 -60
  90. package/transpiled/providers/Memory.js.map +0 -1
@@ -1,4 +1,3 @@
1
- import { Readable } from 'node:stream'
2
1
  import { randomUUID } from 'node:crypto'
3
2
  import { buffer } from 'node:stream/consumers'
4
3
  import { createReadStream } from 'node:fs'
@@ -6,28 +5,22 @@ import path from 'node:path'
6
5
  import assert from 'node:assert'
7
6
  import { Storage } from './Storage'
8
7
  import { suites } from './test/util'
9
- import { type Entry } from './Entry'
10
8
  import { providers } from './providers'
11
- import type { ProviderConstructor } from './Provider'
9
+ import type { Entry, Metadata, Stream } from './Entry'
10
+ import type { Constructor } from './Provider'
12
11
 
13
- let storage: Storage
14
- let dir: string
12
+ jest.setTimeout(15_000)
15
13
 
16
14
  const suite = suites[0]
15
+ const Provider: Constructor = providers[suite.provider]
16
+ const provider = new Provider(suite.options, suite.secrets)
17
+ const storage = new Storage(provider)
18
+ const dir = '/' + randomUUID()
17
19
 
18
20
  beforeAll(async () => {
19
21
  process.chdir(path.join(__dirname, 'test'))
20
22
  })
21
23
 
22
- beforeEach(() => {
23
- dir = '/' + randomUUID()
24
-
25
- const Provider: ProviderConstructor = providers[suite.provider]
26
- const provider = new Provider(suite.options)
27
-
28
- storage = new Storage(provider)
29
- })
30
-
31
24
  it('should be', async () => {
32
25
  expect(storage).toBeInstanceOf(Storage)
33
26
  })
@@ -40,6 +33,7 @@ it('should return error if entry is not found', async () => {
40
33
  })
41
34
 
42
35
  describe('put', () => {
36
+ let path: string
43
37
  let lenna: Entry
44
38
  let startCreation: number
45
39
 
@@ -47,7 +41,10 @@ describe('put', () => {
47
41
  const stream = createReadStream('lenna.png')
48
42
 
49
43
  startCreation = Date.now()
50
- lenna = (await storage.put(dir, stream)) as Entry
44
+ lenna = await storage.put(dir, stream) as Entry
45
+ path = `${dir}/${lenna.id}`
46
+
47
+ assert(!(lenna instanceof Error))
51
48
  })
52
49
 
53
50
  it('should not return error', async () => {
@@ -56,14 +53,7 @@ describe('put', () => {
56
53
 
57
54
  it('should return entry id', async () => {
58
55
  expect(lenna.id).toBeDefined()
59
- })
60
-
61
- it('should return id as checksum', async () => {
62
- const stream = createReadStream('lenna.png')
63
- const dir2 = '/' + randomUUID()
64
- const copy = (await storage.put(dir2, stream)) as Entry
65
-
66
- expect(copy).toHaveProperty('id', lenna.id)
56
+ expect(lenna.id).toHaveLength(32)
67
57
  })
68
58
 
69
59
  it('should detect file type', async () => {
@@ -78,256 +68,75 @@ describe('put', () => {
78
68
  expect(lenna).toMatchObject({
79
69
  id: lenna.id,
80
70
  type: 'image/png',
81
- variants: [],
82
- meta: {}
83
- })
71
+ size: 473831,
72
+ checksum: lenna.checksum,
73
+ created: expect.any(String),
74
+ attributes: {}
75
+ } satisfies Entry)
84
76
  })
85
77
 
86
- it('should create entry', async () => {
87
- const entry = await storage.get(`${dir}/${lenna.id}`)
78
+ it('should create metadata', async () => {
79
+ const entry = await storage.head(path) as Entry
88
80
 
89
81
  expect(entry).toMatchObject({
90
- id: lenna.id,
91
82
  type: 'image/png',
92
- variants: [],
93
- meta: {}
94
- })
83
+ size: 473831,
84
+ checksum: lenna.checksum,
85
+ created: expect.any(String),
86
+ attributes: {}
87
+ } satisfies Metadata)
95
88
  })
96
89
 
97
90
  it('should set timestamp', async () => {
98
- const now = Date.now()
99
- const entry = (await storage.get(`${dir}/${lenna.id}`)) as Entry
100
-
101
- expect(entry.created).toBeLessThanOrEqual(now)
102
- expect(entry.created).toBeGreaterThanOrEqual(startCreation)
103
- })
104
-
105
- describe('existing entry', () => {
106
- it('should unhide existing', async () => {
107
- const stream = createReadStream('lenna.png')
108
- const path = `${dir}/${lenna.id}`
109
-
110
- await storage.conceal(path)
111
- await storage.put(dir, stream)
112
-
113
- const list = await storage.list(dir)
114
-
115
- expect(list).toContainEqual(lenna.id)
116
- })
117
-
118
- it('should preserve meta', async () => {
119
- const path = `${dir}/${lenna.id}`
120
- const stream = createReadStream('lenna.png')
121
-
122
- await storage.annotate(path, 'foo', 'bar')
123
- await storage.put(dir, stream)
124
-
125
- const entry = await storage.get(path)
126
-
127
- expect(entry).toHaveProperty('meta', expect.objectContaining({ foo: 'bar' }))
128
- })
129
- })
130
- })
131
-
132
- describe('list', () => {
133
- let albert: Entry
134
- let lenna: Entry
135
-
136
- beforeEach(async () => {
137
- const stream0 = createReadStream('albert.jpg')
138
- const stream1 = createReadStream('lenna.png')
139
-
140
- albert = (await storage.put(dir, stream0)) as Entry
141
- lenna = (await storage.put(dir, stream1)) as Entry
142
- })
143
-
144
- it('should list entries', async () => {
145
- const list = await storage.list(dir)
146
-
147
- expect(list).toEqual([albert.id, lenna.id])
148
- })
149
-
150
- it('should permutate', async () => {
151
- const error = await storage.permute(dir, [lenna.id, albert.id])
91
+ const now = Date.now() + 1000 // Cloudinary has 1s resolution
92
+ const entry = await storage.get(path) as Stream
93
+ const created = new Date(entry.created).getTime()
152
94
 
153
- expect(error).toBeUndefined()
154
-
155
- const list = await storage.list(dir)
156
-
157
- expect(list).toEqual([lenna.id, albert.id])
158
- })
159
-
160
- it('should return PERMUTATION_MISMATCH', async () => {
161
- const cases = [
162
- [lenna.id],
163
- [albert.id, lenna.id, 'unknown'],
164
- [lenna.id, lenna.id],
165
- [lenna.id, lenna.id, albert.id]
166
- ]
167
-
168
- for (const permutation of cases) {
169
- const error = await storage.permute(dir, permutation)
170
-
171
- expect(error).toBeInstanceOf(Error)
172
- expect(error).toHaveProperty('code', 'PERMUTATION_MISMATCH')
173
- }
174
- })
175
-
176
- it('should exclude concealed', async () => {
177
- const path = `${dir}/${lenna.id}`
178
-
179
- await storage.conceal(path)
180
-
181
- const entries = await storage.list(dir)
182
-
183
- expect(entries).toEqual([albert.id])
184
- })
185
-
186
- it('should reveal', async () => {
187
- const path = `${dir}/${lenna.id}`
188
-
189
- await storage.conceal(path)
190
- await storage.reveal(path)
191
- await storage.reveal(path) // test that no duplicates are created
192
-
193
- const entries = await storage.list(dir)
194
-
195
- expect(entries).toEqual([albert.id, lenna.id])
196
- })
197
-
198
- it('should return ERR_NOT_FOOUD if entry doesnt exist', async () => {
199
- const path = `${dir}/oopsie`
200
-
201
- const methods: Array<'reveal' | 'conceal'> = ['reveal', 'conceal']
202
-
203
- for (const method of methods) {
204
- const error = await storage[method](path)
205
-
206
- expect(error).toBeInstanceOf(Error)
207
- expect(error).toHaveProperty('code', 'NOT_FOUND')
208
- }
95
+ expect(created).toBeLessThanOrEqual(now)
96
+ expect(created).toBeGreaterThanOrEqual(startCreation)
209
97
  })
210
- })
211
98
 
212
- describe('annotate', () => {
213
- let lenna: Entry
214
-
215
- beforeEach(async () => {
99
+ it('should store with given id', async () => {
216
100
  const stream = createReadStream('lenna.png')
101
+ const id = Math.random().toString(36).slice(2)
217
102
 
218
- lenna = (await storage.put(dir, stream)) as Entry
219
- })
220
-
221
- it('should set meta', async () => {
222
- const path = `${dir}/${lenna.id}`
103
+ const entry = await storage.put(dir, stream, { id }) as Entry
223
104
 
224
- await storage.annotate(path, 'foo', 'bar')
105
+ expect(entry.id).toBe(id)
225
106
 
226
- const state0 = (await storage.get(path)) as Entry
107
+ const check = await storage.head(`${dir}/${id}`)
227
108
 
228
- expect(state0).toHaveProperty('meta.foo', 'bar')
109
+ if (check instanceof Error)
110
+ throw check
229
111
 
230
- await storage.annotate(path, 'foo')
231
-
232
- const state1 = (await storage.get(path)) as Entry
233
-
234
- expect(state1.meta).not.toHaveProperty('foo')
112
+ expect(check.id).toBe(id)
235
113
  })
236
114
  })
237
115
 
238
- describe('variants', () => {
116
+ describe('get, head', () => {
239
117
  let lenna: Entry
118
+ let path: string
240
119
 
241
120
  beforeEach(async () => {
242
121
  const stream = createReadStream('lenna.png')
243
122
 
244
- lenna = (await storage.put(dir, stream)) as Entry
245
- })
246
-
247
- it('should add variant', async () => {
248
- const stream = createReadStream('sample.jpeg')
123
+ lenna = await storage.put(dir, stream) as Entry
249
124
 
250
- const path = `${dir}/${lenna.id}`
251
-
252
- await storage.diversify(path, 'foo', stream)
253
-
254
- const state = (await storage.get(path)) as Entry
255
-
256
- expect(state).toHaveProperty('variants',
257
- expect.arrayContaining([{ name: 'foo', size: 73444, type: 'image/jpeg' }]))
125
+ path = `${dir}/${lenna.id}`
258
126
  })
259
127
 
260
- it('should replace variant', async () => {
261
- const stream0 = createReadStream('sample.jpeg')
262
- const stream1 = createReadStream('sample.webp')
263
- const path = `${dir}/${lenna.id}`
264
-
265
- await storage.diversify(path, 'foo', stream0)
266
- await storage.diversify(path, 'foo', stream1)
267
-
268
- const state = (await storage.get(path)) as Entry
269
-
270
- expect(state).toHaveProperty('variants',
271
- expect.arrayContaining([expect.objectContaining({ name: 'foo', type: 'image/webp' })]))
272
- })
273
- })
274
-
275
- describe('fetch', () => {
276
- let lenna: Entry
277
-
278
- beforeEach(async () => {
279
- const stream = createReadStream('lenna.png')
280
-
281
- lenna = (await storage.put(dir, stream)) as Entry
282
- })
283
-
284
- it('should fetch', async () => {
285
- const path = `${dir}/${lenna.id}`
286
- const stream = await storage.fetch(path)
287
-
288
- assert.ok(stream instanceof Readable)
289
-
290
- const stored = await buffer(stream)
128
+ it('should get', async () => {
129
+ const entry = await storage.get(path) as Stream
130
+ const stored = await buffer(entry.stream)
291
131
  const buf = await buffer(createReadStream('lenna.png'))
292
132
 
293
133
  expect(stored.compare(buf)).toBe(0)
294
134
  })
295
135
 
296
- it('should fetch blob by id', async () => {
297
- const stream = createReadStream('lenna.ascii')
298
- const entry = (await storage.put(dir, stream)) as Entry
299
- const stored = await storage.fetch(entry.id)
300
-
301
- if (stored instanceof Error) throw stored
302
-
303
- const buf = await buffer(stored)
304
- const expected = await buffer(createReadStream('lenna.ascii'))
305
-
306
- expect(buf.compare(expected)).toBe(0)
307
- })
308
-
309
- it('should fetch variant', async () => {
310
- const stream = createReadStream('sample.jpeg')
311
-
312
- const buf = await buffer(stream)
313
- const path = `${dir}/${lenna.id}`
314
-
315
- await storage.diversify(path, '100x100.jpeg', Readable.from(buf))
316
-
317
- const variant = await storage.fetch(`${path}.100x100.jpeg`)
318
-
319
- assert.ok(variant instanceof Readable)
320
-
321
- const stored = await buffer(variant)
322
-
323
- expect(stored.compare(buf)).toBe(0)
324
- })
325
-
326
- it('should not fetch blob by id and fake path', async () => {
327
- const stored = await storage.fetch(`fake/${lenna.id}`)
136
+ it('should get entry', async () => {
137
+ const entry = await storage.head(path) as Entry
328
138
 
329
- expect(stored).toBeInstanceOf(Error)
330
- expect(stored).toHaveProperty('code', 'NOT_FOUND')
139
+ expect(entry.id).toBe(lenna.id)
331
140
  })
332
141
  })
333
142
 
@@ -337,46 +146,26 @@ describe('delete', () => {
337
146
  beforeEach(async () => {
338
147
  const stream = createReadStream('lenna.png')
339
148
 
340
- lenna = (await storage.put(dir, stream)) as Entry
341
- })
342
-
343
- it('should remove from the list', async () => {
344
- await storage.delete(`${dir}/${lenna.id}`)
345
-
346
- const list = await storage.list(dir)
347
-
348
- expect(list).not.toContain(lenna.id)
149
+ lenna = await storage.put(dir, stream) as Entry
349
150
  })
350
151
 
351
152
  it('should delete entry', async () => {
352
153
  await storage.delete(`${dir}/${lenna.id}`)
353
154
 
155
+ // Cloudinary needs some time to invalidate the cache
156
+ if (suite.provider === 'cloudinary')
157
+ await new Promise((resolve) => setTimeout(resolve, 10_000))
158
+
354
159
  const result = await storage.get(`${dir}/${lenna.id}`)
355
160
 
356
161
  expect(result).toBeInstanceOf(Error)
357
162
  expect(result).toHaveProperty('code', 'NOT_FOUND')
358
163
  })
359
164
 
360
- it('should delete variants', async () => {
361
- const stream = createReadStream('sample.jpeg')
362
-
363
- const path = `${dir}/${lenna.id}`
364
-
365
- await storage.diversify(path, 'foo', stream)
366
- await storage.delete(`${dir}/${lenna.id}`)
367
- stream.destroy()
368
-
369
- const variant = await storage.fetch(`${path}.foo`)
370
-
371
- expect(variant).toBeInstanceOf(Error)
372
- expect(variant).toHaveProperty('code', 'NOT_FOUND')
373
- })
374
-
375
- it('should throw if path is not an entry', async () => {
165
+ it('should not return error', async () => {
376
166
  const result = await storage.delete(dir)
377
167
 
378
- expect(result).toBeInstanceOf(Error)
379
- expect(result).toMatchObject({ code: 'NOT_FOUND' })
168
+ expect(result).toBeUndefined()
380
169
  })
381
170
  })
382
171
 
@@ -384,14 +173,36 @@ describe('signatures', () => {
384
173
  it.each(['jpeg', 'gif', 'webp', 'heic', 'jxl', 'avif'])('should detect image/%s',
385
174
  async (type) => {
386
175
  const stream = createReadStream('sample.' + type)
176
+ const entry = await storage.put(dir, stream) as Entry
177
+
178
+ expect(entry).toHaveProperty('type', 'image/' + type)
179
+ })
180
+
181
+ it.each(['avi'])('should detect video/%s',
182
+ async (type) => {
183
+ const stream = createReadStream('sample.' + type)
184
+ const entry = await storage.put(dir, stream) as Entry
185
+
186
+ expect(entry).toHaveProperty('type', 'video/' + type)
187
+ })
387
188
 
189
+ it.each(['wav'])('should detect audio/%s',
190
+ async (type) => {
191
+ const stream = createReadStream('sample.' + type)
388
192
  const entry = (await storage.put(dir, stream)) as Entry
389
193
 
390
- expect(entry).toHaveProperty('type', 'image/' + type)
194
+ expect(entry).toHaveProperty('type', 'audio/' + type)
391
195
  })
196
+
197
+ it('should be ok with Arny', async () => {
198
+ const stream = createReadStream('arny.jpg')
199
+ const entry = (await storage.put(dir, stream)) as Entry
200
+
201
+ expect(entry).toHaveProperty('type', 'image/jpeg')
202
+ })
392
203
  })
393
204
 
394
- it("should return error if type doesn't match", async () => {
205
+ it('should return error if type doesn\'t match', async () => {
395
206
  const stream = createReadStream('sample.jpeg')
396
207
 
397
208
  const result = await storage.put(dir, stream, { claim: 'image/png' })
@@ -447,27 +258,40 @@ it('should accept wildcard types', async () => {
447
258
 
448
259
  it('should handle root entries', async () => {
449
260
  const stream = createReadStream('sample.jpeg')
450
-
451
- const result = (await storage.put('hello', stream)) as Entry
261
+ const result = await storage.put('/', stream) as Entry
452
262
 
453
263
  expect(result).not.toBeInstanceOf(Error)
454
264
 
455
- const stored = await storage.fetch(result.id)
265
+ const stored = await storage.get(result.id)
456
266
 
457
267
  expect(stored).not.toBeInstanceOf(Error)
458
268
  })
459
269
 
460
- it('should store empty file', async () => {
461
- const stream = createReadStream('empty.txt')
462
- const result = (await storage.put('empty', stream)) as Entry
270
+ it('should return error of stream size limit exceeded', async () => {
271
+ const stream = createReadStream('sample.jpeg')
272
+ const result = await storage.put(dir, stream, { limit: 1024 })
463
273
 
464
- expect(result).toHaveProperty('size', 0)
274
+ expect(result).toBeInstanceOf(Error)
275
+ expect(result).toHaveProperty('code', 'LIMIT_EXCEEDED')
276
+ })
465
277
 
466
- const stored = (await storage.fetch(result.id)) as Readable
278
+ if (suite.provider !== 'cloudinary')
279
+ it('should add origin attribute', async () => {
280
+ const origin = 'https://example.com/image.jpeg'
281
+ const stream = createReadStream('sample.jpeg')
282
+ const result = await storage.put('/origins', stream, { origin }) as Entry
467
283
 
468
- expect(stored).not.toBeInstanceOf(Error)
284
+ assert.ok(!(result instanceof Error))
285
+
286
+ const entry = await storage.get('/origins/' + result.id)
287
+
288
+ assert.ok(!(entry instanceof Error))
289
+
290
+ expect(entry.attributes.origin).toBe(origin)
291
+ })
469
292
 
470
- const buf = await buffer(stored)
293
+ it('should expose path', () => {
294
+ const path = storage.path()
471
295
 
472
- expect(buf).toHaveLength(0)
296
+ expect(typeof path === 'string' || path === null)
473
297
  })