Skip to content

Commit 18cda3a

Browse files
committed
fix(*): use generics for UploadsIdentifier
1 parent 717880a commit 18cda3a

File tree

7 files changed

+108
-95
lines changed

7 files changed

+108
-95
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ TODO
2929
- [ ] Do URL generation for publicly available disks.
3030
- [ ] Support temporary URLs (e.g. presigned URLs for S3 buckets) for disks that support it
3131
- [ ] Support transfer logic for transferring single uploads from one disk to another and in bulk.
32-
- [ ] Instead of using an `Upload` type that is `any`, we should use generics
3332

3433
## Development
3534

src/lib/Uploads.test.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as fs from 'fs';
22
import { promisify } from 'util';
33
import { DiskDriver, DiskManager } from '@carimus/node-disks';
4-
import { MemoryRepository } from '../support';
4+
import { MemoryRepository, MemoryRepositoryUploadIdentifier } from '../support';
55
import { Uploads } from './Uploads';
66
import { UploadMeta } from '../types';
77

@@ -18,14 +18,14 @@ const disks = {
1818
function setup(): {
1919
diskManager: DiskManager;
2020
repository: MemoryRepository;
21-
uploads: Uploads;
21+
uploads: Uploads<MemoryRepositoryUploadIdentifier>;
2222
} {
2323
const diskManager = new DiskManager(disks);
2424
const repository = new MemoryRepository();
2525
return {
2626
diskManager,
2727
repository,
28-
uploads: new Uploads({
28+
uploads: new Uploads<MemoryRepositoryUploadIdentifier>({
2929
disks: diskManager,
3030
repository,
3131
}),
@@ -134,21 +134,17 @@ test('Uploads service can duplicate an upload, creating a new repository record
134134
const duplicateOldMeta = await uploads.duplicate(original);
135135

136136
// Check the repository for the original file data and the new file data
137-
const originalFileRecord = await repository.find(original.id);
138-
const duplicateFileRecord = await repository.find(duplicate.id);
139-
const duplicateOldMetaFileRecord = await repository.find(
140-
duplicateOldMeta.id,
141-
);
137+
const originalFileRecord = await repository.find(original);
138+
const duplicateFileRecord = await repository.find(duplicate);
139+
const duplicateOldMetaFileRecord = await repository.find(duplicateOldMeta);
142140
expect(originalFileRecord.meta).toMatchObject(files.normal.meta);
143141
expect(duplicateFileRecord.meta).toMatchObject(duplicateMeta);
144142
expect(originalFileRecord).not.toMatchObject(duplicateFileRecord);
145143
expect(duplicateOldMetaFileRecord.meta).toMatchObject(files.normal.meta);
146144

147145
// Check the disk for the original file and the new file
148-
const originalFileInfo = await repository.getUploadedFileInfo(original.id);
149-
const duplicateFileInfo = await repository.getUploadedFileInfo(
150-
duplicate.id,
151-
);
146+
const originalFileInfo = await repository.getUploadedFileInfo(original);
147+
const duplicateFileInfo = await repository.getUploadedFileInfo(duplicate);
152148
const originalFileData = await diskManager
153149
.getDisk(originalFileInfo.disk)
154150
.read(originalFileInfo.path);
@@ -187,7 +183,7 @@ test('Uploads service can update, updating existing repository records and delet
187183
const updatedNewMetaFileInfo = await repository.getUploadedFileInfo(
188184
updatedNewMeta,
189185
);
190-
expect(original.id).toBe(updatedNewMeta.id);
186+
expect(original).toBe(updatedNewMeta);
191187
await expect(
192188
diskManager.getDisk(originalFileInfo.disk).read(originalFileInfo.path),
193189
).rejects.toBeTruthy();
@@ -290,12 +286,15 @@ test('Uploads service can create temp files for local manipulation from uploads'
290286
.read(fileInfo.path);
291287

292288
// Get the temp file for it and check to make sure their contents match
293-
const tempPath = await uploads.withTempFile(upload, async (path) => {
294-
const tempFileData = await readFileFromLocalFilesystem(path);
295-
expect(tempFileData.toString('base64')).toBe(
296-
uploadedFileData.toString('base64'),
297-
);
298-
});
289+
const tempPath = await uploads.withTempFile(
290+
upload,
291+
async (path: string) => {
292+
const tempFileData = await readFileFromLocalFilesystem(path);
293+
expect(tempFileData.toString('base64')).toBe(
294+
uploadedFileData.toString('base64'),
295+
);
296+
},
297+
);
299298

300299
// Ensure that once the callback is completed, the file doesn't exist since we didn't tell it not to cleanup
301300
expect(tempPath).toBeTruthy();

src/lib/Uploads.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import * as fs from 'fs';
33
import { DiskManager, pipeStreams } from '@carimus/node-disks';
44
import { InvalidConfigError, PathNotUniqueError } from '../errors';
55
import {
6-
Upload,
76
UploadedFile,
87
UploadMeta,
98
UploadRepository,
@@ -19,12 +18,12 @@ import { defaultSanitizeFilename, defaultGeneratePath } from './defaults';
1918
* TODO Support temporary URLs (e.g. presigned URLs for S3 buckets) for disks that support it
2019
* TODO Support transfer logic for transferring single uploads from one disk to another and in bulk.
2120
*/
22-
export class Uploads {
23-
private config: UploadsConfig;
24-
private repository: UploadRepository;
21+
export class Uploads<UploadIdentifier> {
22+
private config: UploadsConfig<UploadIdentifier>;
23+
private repository: UploadRepository<UploadIdentifier>;
2524
private disks: DiskManager;
2625

27-
public constructor(config: UploadsConfig) {
26+
public constructor(config: UploadsConfig<UploadIdentifier>) {
2827
this.config = config;
2928
this.repository = config.repository;
3029
if (typeof config.disks === 'object') {
@@ -160,7 +159,7 @@ export class Uploads {
160159
fileData: Buffer | Readable,
161160
uploadedAs: string,
162161
meta?: UploadMeta,
163-
): Promise<Upload> {
162+
): Promise<UploadIdentifier> {
164163
const uploadedFile = await this.place(fileData, uploadedAs);
165164
return this.repository.create(uploadedFile, meta);
166165
}
@@ -175,11 +174,11 @@ export class Uploads {
175174
* @param meta
176175
*/
177176
public async update(
178-
upload: Upload,
177+
upload: UploadIdentifier,
179178
fileData: Buffer | Readable,
180179
uploadedAs: string,
181180
meta?: UploadMeta,
182-
): Promise<Upload> {
181+
): Promise<UploadIdentifier> {
183182
// Get the info about the old file and resolve its disk.
184183
const oldFile = await this.repository.getUploadedFileInfo(upload);
185184
const oldDisk = this.disks.getDisk(oldFile.disk);
@@ -206,9 +205,9 @@ export class Uploads {
206205
* @param meta
207206
*/
208207
public async duplicate(
209-
original: Upload,
208+
original: UploadIdentifier,
210209
meta?: UploadMeta,
211-
): Promise<Upload> {
210+
): Promise<UploadIdentifier> {
212211
// Ask the repository for info on where and how the original upload file is stored.
213212
const originalFile = await this.repository.getUploadedFileInfo(
214213
original,
@@ -231,7 +230,7 @@ export class Uploads {
231230
* @param onlyFile If true, only thd disk file is deleted and not the upload in the repository.
232231
*/
233232
public async delete(
234-
upload: Upload,
233+
upload: UploadIdentifier,
235234
onlyFile: boolean = false,
236235
): Promise<void> {
237236
// Ask the repository for info on where and how the upload file is stored.
@@ -265,7 +264,7 @@ export class Uploads {
265264
* @param execute
266265
*/
267266
public async withTempFile(
268-
upload: Upload,
267+
upload: UploadIdentifier,
269268
execute: ((path: string) => Promise<void> | void) | null = null,
270269
): Promise<string> {
271270
// Ask the repository for info on where and how the upload file is stored.

src/support/MemoryRepository.test.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -27,35 +27,27 @@ const bar: { file: UploadedFile; meta: UploadMeta } = {
2727

2828
test('MemoryRepository implements create, update, delete, and getUploadedFileInfo', async () => {
2929
const repo = new MemoryRepository();
30-
const upload = await repo.create(foo.file, foo.meta);
31-
expect(await repo.getUploadedFileInfo(upload)).toMatchObject(foo.file);
32-
const updatedUpload = await repo.update(upload, bar.file, bar.meta);
30+
const uploadId = await repo.create(foo.file, foo.meta);
31+
expect(await repo.getUploadedFileInfo(uploadId)).toMatchObject(foo.file);
32+
const updatedUploadId = await repo.update(uploadId, bar.file, bar.meta);
3333
// Note that we expect `Upload`s to be long-lived identifiers (or contain such) so we test that here by
3434
// checking to ensure the file info is updated for our old upload reference too.
35-
expect(await repo.getUploadedFileInfo(upload)).toMatchObject(bar.file);
36-
expect(await repo.getUploadedFileInfo(updatedUpload)).toMatchObject(
35+
expect(await repo.getUploadedFileInfo(uploadId)).toMatchObject(bar.file);
36+
expect(await repo.getUploadedFileInfo(updatedUploadId)).toMatchObject(
3737
bar.file,
3838
);
3939
// `delete` on success resolves with undefined.
40-
expect(repo.delete(upload)).resolves.toBeUndefined();
40+
expect(repo.delete(uploadId)).resolves.toBeUndefined();
4141
// Since upload identifiers should be long-lived identifiers, this should reject since it's already been deleted.
42-
expect(repo.delete(updatedUpload)).rejects.toBeTruthy();
43-
});
44-
45-
test('MemoryRepository works with IDs as upload identifiers instead of entire records', async () => {
46-
const repo = new MemoryRepository();
47-
const fooUpload = await repo.create(foo.file, foo.meta);
48-
const barUpload = await repo.create(bar.file, bar.meta);
49-
expect(await repo.getUploadedFileInfo(fooUpload.id)).toBe(foo.file);
50-
expect(await repo.getUploadedFileInfo(barUpload.id)).toBe(bar.file);
42+
expect(repo.delete(updatedUploadId)).rejects.toBeTruthy();
5143
});
5244

5345
test('MemoryRepository update handles optional meta by persisting old meta', async () => {
5446
const repo = new MemoryRepository();
55-
const fooUpload = await repo.create(foo.file, foo.meta);
56-
const fooUpload2 = await repo.update(fooUpload, bar.file);
57-
expect(fooUpload.file).toMatchObject(foo.file);
58-
expect(fooUpload.meta).toMatchObject(foo.meta);
59-
expect(fooUpload2.file).toMatchObject(bar.file);
60-
expect(fooUpload2.meta).toMatchObject(foo.meta);
47+
const fooUploadId = await repo.create(foo.file, foo.meta);
48+
expect(await repo.getUploadedFileInfo(fooUploadId)).toMatchObject(foo.file);
49+
expect(await repo.getMeta(fooUploadId)).toMatchObject(foo.meta);
50+
await repo.update(fooUploadId, bar.file);
51+
expect(await repo.getUploadedFileInfo(fooUploadId)).toMatchObject(bar.file);
52+
expect(await repo.getMeta(fooUploadId)).toMatchObject(foo.meta);
6153
});

src/support/MemoryRepository.ts

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
import { UploadedFile, UploadMeta, UploadRepository } from '../types';
22

3+
export type MemoryRepositoryUploadIdentifier = number;
4+
35
export interface MemoryRepositoryRecord {
4-
id: number;
6+
id: MemoryRepositoryUploadIdentifier;
57
file: UploadedFile;
68
meta: UploadMeta;
79
}
810

9-
type MemoryRepositoryUpload = MemoryRepositoryRecord | number;
10-
11-
export class MemoryRepository implements UploadRepository {
12-
private database: Map<number, MemoryRepositoryRecord> = new Map();
13-
private counter: number = 0;
11+
export class MemoryRepository
12+
implements UploadRepository<MemoryRepositoryUploadIdentifier> {
13+
private database: Map<
14+
MemoryRepositoryUploadIdentifier,
15+
MemoryRepositoryRecord
16+
> = new Map();
17+
private counter: MemoryRepositoryUploadIdentifier = 0;
1418

1519
/**
1620
* Get the next available ID. Mimics auto-increment starting at 1.
1721
*/
18-
private getNextID(): number {
22+
private getNextID(): MemoryRepositoryUploadIdentifier {
1923
return ++this.counter;
2024
}
2125

@@ -27,21 +31,22 @@ export class MemoryRepository implements UploadRepository {
2731
public async create(
2832
file: UploadedFile,
2933
meta?: UploadMeta,
30-
): Promise<MemoryRepositoryRecord> {
34+
): Promise<MemoryRepositoryUploadIdentifier> {
3135
const id = this.getNextID();
3236
const upload = { id, file, meta: meta || {} };
3337
this.database.set(id, upload);
34-
return upload;
38+
return upload.id;
3539
}
3640

3741
/**
3842
* Resolve a record or ID to a fresh record straight from the database.
39-
* @param upload
43+
* @param id
4044
*/
41-
private resolve(upload: MemoryRepositoryUpload): MemoryRepositoryRecord {
42-
const id = (typeof upload === 'number' ? upload : upload.id) || null;
45+
private resolve(
46+
id: MemoryRepositoryUploadIdentifier,
47+
): MemoryRepositoryRecord {
4348
if (!id) {
44-
throw new Error(`Bad identifier or malformed record: ${id}`);
49+
throw new Error(`Bad identifier: ${id}`);
4550
}
4651
const existingUpload = this.database.get(id);
4752
if (!existingUpload) {
@@ -57,25 +62,27 @@ export class MemoryRepository implements UploadRepository {
5762
* @param meta
5863
*/
5964
public async update(
60-
upload: MemoryRepositoryUpload,
65+
upload: MemoryRepositoryUploadIdentifier,
6166
file: UploadedFile,
6267
meta?: UploadMeta,
63-
): Promise<MemoryRepositoryRecord> {
68+
): Promise<MemoryRepositoryUploadIdentifier> {
6469
const existingUpload = this.resolve(upload);
6570
const updatedUpload = {
6671
...existingUpload,
6772
file,
6873
meta: meta || existingUpload.meta || {},
6974
};
7075
this.database.set(existingUpload.id, updatedUpload);
71-
return updatedUpload;
76+
return updatedUpload.id;
7277
}
7378

7479
/**
7580
* @inheritDoc
7681
* @param upload
7782
*/
78-
public async delete(upload: MemoryRepositoryUpload): Promise<void> {
83+
public async delete(
84+
upload: MemoryRepositoryUploadIdentifier,
85+
): Promise<void> {
7986
const { id } = this.resolve(upload);
8087
this.database.delete(id);
8188
}
@@ -85,7 +92,7 @@ export class MemoryRepository implements UploadRepository {
8592
* @param upload
8693
*/
8794
public async getUploadedFileInfo(
88-
upload: MemoryRepositoryUpload,
95+
upload: MemoryRepositoryUploadIdentifier,
8996
): Promise<UploadedFile> {
9097
const existingUpload = this.resolve(upload);
9198
return existingUpload.file;
@@ -95,7 +102,9 @@ export class MemoryRepository implements UploadRepository {
95102
* @inheritDoc
96103
* @param upload
97104
*/
98-
public async getMeta(upload: MemoryRepositoryUpload): Promise<UploadMeta> {
105+
public async getMeta(
106+
upload: MemoryRepositoryUploadIdentifier,
107+
): Promise<UploadMeta> {
99108
const existingUpload = this.resolve(upload);
100109
return existingUpload.meta;
101110
}
@@ -105,7 +114,7 @@ export class MemoryRepository implements UploadRepository {
105114
* @param upload
106115
*/
107116
public async find(
108-
upload: MemoryRepositoryUpload,
117+
upload: MemoryRepositoryUploadIdentifier,
109118
): Promise<MemoryRepositoryRecord> {
110119
return this.resolve(upload);
111120
}
@@ -115,13 +124,22 @@ export class MemoryRepository implements UploadRepository {
115124
*
116125
* @param id
117126
*/
118-
public log(id?: number): void {
119-
const rawEntries: [number, MemoryRepositoryRecord | undefined][] = id
120-
? [[id, this.database.get(id)]]
121-
: [...this.database.entries()];
122-
const entries: [number, MemoryRepositoryRecord][] = rawEntries.filter(
123-
(entry: [number, MemoryRepositoryRecord | undefined]) => !!entry[1],
124-
) as [number, MemoryRepositoryRecord][];
127+
public log(id?: MemoryRepositoryUploadIdentifier): void {
128+
const rawEntries: [
129+
MemoryRepositoryUploadIdentifier,
130+
MemoryRepositoryRecord | undefined
131+
][] = id ? [[id, this.database.get(id)]] : [...this.database.entries()];
132+
const entries: [
133+
MemoryRepositoryUploadIdentifier,
134+
MemoryRepositoryRecord
135+
][] = rawEntries.filter(
136+
(
137+
entry: [
138+
MemoryRepositoryUploadIdentifier,
139+
MemoryRepositoryRecord | undefined
140+
],
141+
) => !!entry[1],
142+
) as [MemoryRepositoryUploadIdentifier, MemoryRepositoryRecord][];
125143
console.table(
126144
entries.map(([id, record]) => {
127145
return { id, ...record.file, meta: record.meta };

src/support/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
export { MemoryRepository } from './MemoryRepository';
1+
export {
2+
MemoryRepository,
3+
MemoryRepositoryUploadIdentifier,
4+
} from './MemoryRepository';

0 commit comments

Comments
 (0)