Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(file-uploader): added upload router #222

Merged
merged 44 commits into from
Apr 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
dac5aad
feat(file-uploader): added upload router
slaveeks Mar 10, 2024
bdb7de7
feat: added file sixe limit to config
slaveeks Mar 10, 2024
bd9a6cb
fix: fixed code style
slaveeks Mar 10, 2024
f21fabf
feat: added s3 and file limit to app.config.yaml
slaveeks Mar 10, 2024
3f7974b
refactor: added comment
slaveeks Mar 10, 2024
9f85c50
refactor: some refactor changes due to review
slaveeks Mar 13, 2024
8e24eb4
refactor: changed UploadFile service method arguments
slaveeks Mar 13, 2024
85eda1a
refactor: removed useless import, added doc for key in service
slaveeks Mar 13, 2024
de24625
refactor: use fileType in policy, changed uploaded file response to o…
slaveeks Mar 13, 2024
cfacf3f
refactor: some changes due to review
slaveeks Mar 13, 2024
4060b66
refactor: removed empty return from policy
slaveeks Mar 13, 2024
8b5e0f3
refactor: fill config default
slaveeks Mar 13, 2024
74ebe10
refactor: use notEmpty
slaveeks Mar 13, 2024
fbe44bd
refactor: replaced { note } = request
slaveeks Mar 13, 2024
6492d91
refactor: made enum key with capital first letter
slaveeks Apr 1, 2024
5ead961
fix: make FileType enum value as integer, removed changed type column…
slaveeks Apr 1, 2024
4e1c893
feat: added file location types
slaveeks Apr 1, 2024
055edd8
refactor: not use hardcoded location
slaveeks Apr 1, 2024
fedcc19
feat: add location column to files, removed note_id column
slaveeks Apr 1, 2024
4dbcfe2
feat: make upload file location depended on type
slaveeks Apr 2, 2024
518905b
feat: created method for location validation due to passes file type
slaveeks Apr 2, 2024
db0a070
feat: added getting file location by key and file type
slaveeks Apr 2, 2024
0a6a01d
Merge branch 'main' of github.com:codex-team/notes.api into feat/add-…
slaveeks Apr 2, 2024
66703dd
feat: upgrade reading file access
slaveeks Apr 2, 2024
caed061
refactor: renamed policy for checking access for uploading
slaveeks Apr 2, 2024
df63207
feat: upgrade checking user access for uploading
slaveeks Apr 2, 2024
c533e56
Update src/domain/service/fileUploader.service.ts
slaveeks Apr 2, 2024
112ea2b
Update src/domain/service/fileUploader.service.ts
slaveeks Apr 2, 2024
d63ad76
fix: changed default file limit
slaveeks Apr 2, 2024
d0bb6c5
Merge branch 'feat/add-upload-router' of github.com:codex-team/notes.…
slaveeks Apr 2, 2024
315f9a8
Update src/domain/service/fileUploader.service.ts
slaveeks Apr 3, 2024
eb4816a
Apply suggestions from code review
slaveeks Apr 3, 2024
3a6e1a4
refactor: change file conditional type
slaveeks Apr 3, 2024
07d769e
refactor: register fastify multipart into the upload router
slaveeks Apr 3, 2024
6db0ae8
refactor: change validation of lcoation
slaveeks Apr 3, 2024
075a9e8
fix: fixed importing types in userCanUploadPolicy
slaveeks Apr 3, 2024
f31b705
feat: added uploading and getting file for defined note
slaveeks Apr 6, 2024
22d8d1a
refactor: removed passing fileType to upload service methods
slaveeks Apr 6, 2024
9aa9db5
refactor: updated uploadFile docs
slaveeks Apr 6, 2024
b43ff1f
refactor: updated doc
slaveeks Apr 6, 2024
8e832f3
refactor: removed redundant policies
slaveeks Apr 6, 2024
f973a7f
Apply suggestions from code review
slaveeks Apr 6, 2024
020be87
feat: add metadata column
slaveeks Apr 6, 2024
1c06f0f
Apply suggestions from code review
slaveeks Apr 6, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app-config.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
httpApi:
host: 0.0.0.0
fileSizeLimit: 1000000
port: 1337
allowedOrigins: '*'
cookieSecret: 'secret'
Expand Down Expand Up @@ -27,6 +28,11 @@ logging:
appServer: info
database: info

s3:
accessKeyId: 'secret'
secretAccessKey: 'secret'
endpoint: 'http://localhost:9000'

database:
dsn: 'postgres://codex:postgres@postgres:5432/notes'

Expand Down
72 changes: 72 additions & 0 deletions migrations/tenant/0028-files@add-column-size.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
DO $$
BEGIN
-- Add sixe column if it not exists
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'files'
AND column_name = 'size'
)
THEN
ALTER TABLE files ADD COLUMN size integer NOT NULL;
END IF;

-- Make id column autoincrementing
CREATE SEQUENCE IF NOT EXISTS public.files_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;


-- Make identifier default value incrementing
ALTER TABLE ONLY public.files ALTER COLUMN id SET DEFAULT nextval('public.files_id_seq'::regclass);

-- Remove column note_id
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'files'
AND column_name = 'note_id'
) THEN
-- Drop the column if it exists
ALTER TABLE files
DROP COLUMN note_id;
END IF;

-- Add column location
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'files'
AND column_name = 'location'
) THEN
ALTER TABLE files
ADD COLUMN location JSONB;
END IF;

-- Remove column user_id
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'files'
AND column_name = 'user_id'
) THEN
-- Drop the column if it exists
ALTER TABLE files
DROP COLUMN user_id;
END IF;

-- Add column metadata
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'files'
AND column_name = 'metadata'
) THEN
ALTER TABLE files
ADD COLUMN metadata JSONB;
END IF;
END $$;
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@codex-team/config-loader": "^1.0.0",
"@fastify/cookie": "^8.3.0",
"@fastify/cors": "^8.3.0",
"@fastify/multipart": "^8.1.0",
"@fastify/oauth2": "^7.2.1",
"@fastify/swagger": "^8.8.0",
"@fastify/swagger-ui": "^1.9.3",
Expand All @@ -48,6 +49,7 @@
"fastify": "^4.17.0",
"http-status-codes": "^2.2.0",
"jsonwebtoken": "^9.0.0",
"mime": "^4.0.1",
"nanoid": "^4.0.2",
"pg": "^8.11.0",
"pg-hstore": "^2.3.4",
Expand Down
64 changes: 52 additions & 12 deletions src/domain/entities/file.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,55 @@
import type { NoteInternalId } from './note.js';
import type User from './user.js';
import type { Buffer } from 'buffer';
import type User from './user.js';

/**
* File types for storing in object storage
*/
export enum FileTypes {
export enum FileType {
/**
* Type for testing uploads
*/
Test = 0,

/**
* @todo define real types
* File is a part of note
*/
test = 'test',
NoteAttachment = 1,
}

/**
* Additional data about uploaded file, ex. user id, who uploaded it
*/
export interface FileMetadata {
/**
* User who uploaded file
*/
userId: User['id'];
}

/**
* File location for testing uploads, there is no defined location
*/
export type TestFileLocation = Record<never, never>;

/**
* File location, when it is a part of note
*/
export type NoteAttachmentFileLocation = {
noteId: NoteInternalId,
};

/**
* Possible file location
*/
export type FileLocation = TestFileLocation | NoteAttachmentFileLocation;

/**
* File location type, wich depends on file type
*/
export interface FileLocationByType {
[FileType.Test]: TestFileLocation,
[FileType.NoteAttachment]: NoteAttachmentFileLocation,
}

/**
Expand All @@ -26,11 +66,6 @@ export default interface UploadedFile {
*/
key: string;

/**
* User who uploaded the file
*/
userId?: User['id'];

/**
* File name
*/
Expand All @@ -44,7 +79,7 @@ export default interface UploadedFile {
/**
* File type, using to store in object storage
*/
type: FileTypes;
type: FileType;

/**
* File size in bytes
Expand All @@ -57,9 +92,14 @@ export default interface UploadedFile {
createdAt: Date;

/**
* In case if file is a part of note, note id to identify permissions to access
* Object, which stores information about file location
*/
location: FileLocation;

/**
* File metadata
*/
noteId?: NoteInternalId;
metadata: FileMetadata;
}

/**
Expand Down
120 changes: 84 additions & 36 deletions src/domain/service/fileUploader.service.ts
elizachi marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { FileData } from '@domain/entities/file.js';
import { FileTypes } from '@domain/entities/file.js';
import type { NoteInternalId } from '@domain/entities/note.js';
import type User from '@domain/entities/user.js';
import type { FileData, NoteAttachmentFileLocation, FileLocationByType, FileLocation, FileMetadata } from '@domain/entities/file.js';
import type UploadedFile from '@domain/entities/file.js';
import { FileType } from '@domain/entities/file.js';
import { createFileId } from '@infrastructure/utils/id.js';
import type FileRepository from '@repository/file.repository.js';
import type ObjectRepository from '@repository/object.repository.js';
import { DomainError } from '@domain/entities/DomainError.js';
import mime from 'mime';

/**
* File data for upload
*/
interface FileToUpload {
interface UploadFileData {
/**
* File data
*/
Expand All @@ -23,25 +23,6 @@ interface FileToUpload {
* Mimetype of the file
*/
mimetype: string;
/**
* File type
*/
type: FileTypes;
}

/**
* File upload details, contains user id, who uploaded the file and note id, in case if file is a part of note
*/
interface FileUploadDetails {
/**
* User who uploaded the file
*/
userId?: User['id'];

/**
* In case if file is a part of note, note id to identify permissions to access
*/
noteId?: NoteInternalId;
}

/**
Expand Down Expand Up @@ -72,13 +53,30 @@ export default class FileUploaderService {
/**
* Upload file
*
* @param fileData - file data to upload (e.g. buffer, name, mimetype)
* @param details - file upload details (e.g. user id, note id)
* @param fileData - file data, including file data, name and mimetype
* @param location - file location depending on type
* @param metadata - file metadata, including user id who uploaded the file
*/
public async uploadFile(fileData: FileToUpload, details?: FileUploadDetails): Promise<string> {
const key = createFileId();
public async uploadFile(fileData: UploadFileData, location: FileLocation, metadata: FileMetadata): Promise<string> {
const type = this.defineFileType(location);

const fileHash = createFileId();

/**
* Extension can be null if file mime type is unknown or not supported
*/
const fileExtension = mime.getExtension(fileData.mimetype);

GoldenJaden marked this conversation as resolved.
Show resolved Hide resolved
if (fileExtension === null) {
throw new DomainError('Unknown file extension');
}

const bucket = this.defineBucketByFileType(fileData.type);
/**
* Key is a combination of file hash and file extension, separated by dot, e.g. `HgduSDGmsdrs.png`
*/
const key = `${fileHash}.${fileExtension}`;

elizachi marked this conversation as resolved.
Show resolved Hide resolved
const bucket = this.defineBucketByFileType(type);

const uploaded = await this.objectRepository.insert(fileData.data, key, bucket);

Expand All @@ -88,25 +86,50 @@ export default class FileUploaderService {

const file = await this.fileRepository.insert({
...fileData,
type,
key,
userId: details?.userId,
noteId: details?.noteId,
metadata,
location: location,
size: fileData.data.length,
});

if (file === null) {
throw new Error('File was not uploaded');
throw new DomainError('File was not uploaded');
}

return file.key;
}

/**
* Get file location by key and type
* Returns null if where is no such file
*
* @param type - file type
* @param key - file unique key
*/
public async getFileLocationByKey<T extends FileType>(type: T, key: UploadedFile['key']): Promise<FileLocationByType[T] | null> {
return await this.fileRepository.getFileLocationByKey(type, key);
}

/**
* Get file data by key
*
* @param objectKey - unique file key in object storage
* @param location - file location
*/
public async getFileDataByKey(objectKey: string): Promise<FileData> {
public async getFileData(objectKey: string, location: FileLocation): Promise<FileData> {
/**
* If type of requested file is note attchement, we need to check if saved file location is the same of requested
*/
if (this.isNoteAttachemntFileLocation(location)) {
const fileType = FileType.NoteAttachment;
const fileLocationFromStorage = await this.fileRepository.getFileLocationByKey(fileType, objectKey);

if (fileLocationFromStorage === null || location.noteId !== fileLocationFromStorage.noteId) {
throw new DomainError('File not found');
}
}

const file = await this.fileRepository.getByKey(objectKey);

if (file === null) {
Expand All @@ -124,17 +147,42 @@ export default class FileUploaderService {
return fileData;
}


/**
* Define file type by location
*
* @param location - file location
*/
private defineFileType(location: FileLocation): FileType {
if (this.isNoteAttachemntFileLocation(location)) {
return FileType.NoteAttachment;
}

return FileType.Test;
}

/**
* Check if file location is note attachemnt
*
* @param location - to check
*/
private isNoteAttachemntFileLocation(location: FileLocation): location is NoteAttachmentFileLocation {
return 'noteId' in location;
}

/**
* Define bucket name by file type
*
* @param fileType - file type
*/
private defineBucketByFileType(fileType: FileTypes): string {
private defineBucketByFileType(fileType: FileType): string {
switch (fileType) {
case FileTypes.test:
case FileType.Test:
return 'test';
case FileType.NoteAttachment:
return 'note-attachment';
default:
throw new Error('Unknown file type');
throw new DomainError('Unknown file type');
}
}
}
Loading
Loading