-
Notifications
You must be signed in to change notification settings - Fork 114
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(asset): create asset functionality
- Loading branch information
Showing
24 changed files
with
885 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,8 @@ | ||
DATABASE_URL=postgresql://localhost:5432/prime | ||
TEST_DATABASE_URL=postgresql://localhost:5432/prime-test | ||
SESSION_SECRET=keyboard-cat-dart | ||
|
||
S3_BUCKET= | ||
S3_REGION= | ||
S3_ACCESS_KEY_ID= | ||
S3_SECRET_ACCESS_KEY= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import { User } from '@accounts/typeorm'; | ||
import { GraphQLFloat } from 'graphql'; | ||
import { Field, ID, ObjectType } from 'type-graphql'; | ||
import { | ||
Column, | ||
CreateDateColumn, | ||
Entity, | ||
ManyToOne, | ||
PrimaryGeneratedColumn, | ||
UpdateDateColumn, | ||
} from 'typeorm'; | ||
|
||
@Entity() | ||
@ObjectType() | ||
export class Asset { | ||
@Field(type => ID) | ||
@PrimaryGeneratedColumn('uuid') | ||
public id: string; | ||
|
||
@CreateDateColumn() | ||
@Field({ nullable: true }) | ||
public createdAt: Date; | ||
|
||
@UpdateDateColumn() | ||
@Field({ nullable: true }) | ||
public updatedAt: Date; | ||
|
||
@Column({ type: 'timestamp', nullable: true }) | ||
@Field({ nullable: true }) | ||
public deletedAt: Date; | ||
|
||
@Column({ nullable: false }) | ||
@Field({ nullable: false }) | ||
public url: string; | ||
|
||
@Column({ nullable: false }) | ||
@Field({ nullable: false }) | ||
public mimeType: string; | ||
|
||
@Column({ nullable: false }) | ||
@Field({ nullable: false }) | ||
public handle: string; | ||
|
||
@Column({ nullable: false }) | ||
@Field({ nullable: false }) | ||
public fileName: string; | ||
|
||
@Column({ nullable: false }) | ||
@Field({ nullable: false }) | ||
public fileSize: number; | ||
|
||
@Column('float', { nullable: true }) | ||
@Field(type => GraphQLFloat, { nullable: true }) | ||
public width: number; | ||
|
||
@Column('float', { nullable: true }) | ||
@Field(type => GraphQLFloat, { nullable: true }) | ||
public height: number; | ||
|
||
@Column({ nullable: true }) | ||
@Field({ nullable: true }) | ||
public userId?: string; | ||
|
||
@ManyToOne(type => User, { nullable: true, onDelete: 'SET NULL' }) | ||
public user: User; | ||
} |
23 changes: 23 additions & 0 deletions
23
packages/prime-core/src/modules/external/interfaces/Upload.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { ReadStream } from 'fs'; | ||
|
||
/** | ||
* File upload details, resolved from an `Upload` scalar promise. | ||
* See: https://github.com/jaydenseric/graphql-upload | ||
*/ | ||
export interface Upload { | ||
/** File name */ | ||
filename: string; | ||
|
||
/** File MIME type. Provided by the client and can’t be trusted. */ | ||
mimetype: string; | ||
|
||
/** File stream transfer encoding. */ | ||
encoding: string; | ||
|
||
/** | ||
* createReadStream Returns a Node.js readable stream of the file contents, | ||
* for processing and storing the file. Multiple calls create independent streams. | ||
* Throws if called after all resolvers have resolved, or after an error has interrupted the request. | ||
*/ | ||
createReadStream: () => ReadStream; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
packages/prime-core/src/modules/internal/repositories/AssetRepository.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import { EntityRepository } from 'typeorm'; | ||
import { Asset } from '../../../entities/Asset'; | ||
import { DataLoaderRepository } from './DataLoaderRepository'; | ||
|
||
@EntityRepository(Asset) | ||
export class AssetRepository extends DataLoaderRepository<Asset> {} |
111 changes: 111 additions & 0 deletions
111
packages/prime-core/src/modules/internal/resolvers/AssetResolver.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { Context } from 'apollo-server-core'; | ||
import { GraphQLResolveInfo } from 'graphql'; | ||
import { | ||
Arg, | ||
Args, | ||
Ctx, | ||
FieldResolver, | ||
ID, | ||
Info, | ||
Mutation, | ||
Query, | ||
registerEnumType, | ||
Resolver, | ||
Root, | ||
} from 'type-graphql'; | ||
import Container from 'typedi'; | ||
import { getRepository } from 'typeorm'; | ||
import { EntityConnection } from 'typeorm-cursor-connection'; | ||
import { InjectRepository } from 'typeorm-typedi-extensions'; | ||
import { Asset } from '../../../entities/Asset'; | ||
import { S3Storage } from '../../../utils/S3Storage'; | ||
import { AssetRepository } from '../repositories/AssetRepository'; | ||
import { AssetInput } from '../types/AssetInput'; | ||
import { ConnectionArgs, createConnectionType } from '../types/createConnectionType'; | ||
import { User } from '../types/User'; | ||
import { Authorized } from '../utils/Authorized'; | ||
|
||
const AssetConnection = createConnectionType(Asset); | ||
|
||
enum AssetOrder { | ||
updatedAt_ASC, | ||
updatedAt_DESC, | ||
id_ASC, | ||
id_DESC, | ||
fileName_ASC, | ||
fileName_DESC, | ||
} | ||
|
||
registerEnumType(AssetOrder, { | ||
name: 'AssetConnectionOrder', | ||
}); | ||
|
||
@Resolver(of => Asset) | ||
export class AssetResolver { | ||
@InjectRepository(AssetRepository) | ||
private readonly assetRepository: AssetRepository; | ||
private readonly storage: S3Storage; | ||
|
||
constructor() { | ||
this.storage = Container.get(S3Storage); | ||
} | ||
|
||
@Authorized() | ||
@Query(returns => Asset, { nullable: true, description: 'Get Asset by ID' }) | ||
public Asset( | ||
@Arg('id', type => ID) id: string, | ||
@Ctx() context: Context, | ||
@Info() info: GraphQLResolveInfo | ||
) { | ||
return this.assetRepository.loadOne(id); | ||
} | ||
|
||
@Authorized() | ||
@Query(returns => AssetConnection, { description: 'Get many Assets' }) | ||
public async allAssets( | ||
@Args() args: ConnectionArgs, | ||
@Arg('order', type => AssetOrder, { defaultValue: 0 }) orderBy: string | ||
) { | ||
const [sort, order]: any = orderBy.split('_'); | ||
const connection = await new EntityConnection(args, { | ||
repository: this.assetRepository, | ||
sortOptions: [{ sort: `Asset.${sort}`, order }], | ||
}); | ||
return { | ||
edges: await connection.edges, | ||
totalCount: await this.assetRepository.count(), | ||
}; | ||
} | ||
|
||
@Authorized() | ||
@Mutation(returns => Asset, { description: 'Create Asset' }) | ||
public async createAsset( | ||
@Arg('input') input: AssetInput, | ||
@Ctx() context: Context | ||
): Promise<Asset> { | ||
const upload = await input.upload; | ||
const assetInput = await this.storage.upload(upload.filename, upload.createReadStream()); | ||
const asset = this.assetRepository.create(assetInput); | ||
await this.assetRepository.save(asset); | ||
return asset; | ||
} | ||
|
||
@Authorized() | ||
@Mutation(returns => Boolean, { description: 'Remove Asset by ID' }) | ||
public async removeAsset( | ||
@Arg('id', type => ID) id: string, | ||
@Ctx() context: Context | ||
): Promise<boolean> { | ||
const entity = await this.assetRepository.findOneOrFail(id); | ||
await this.storage.delete(entity.fileName); | ||
return Boolean(await this.assetRepository.remove(entity)); | ||
} | ||
|
||
@FieldResolver(returns => User, { description: 'Get Asset User' }) | ||
public user(@Root() asset: Asset): Promise<User> { | ||
return getRepository(User).findOneOrFail({ | ||
cache: 1000, | ||
where: asset.user, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { GraphQLUpload } from 'graphql-upload'; | ||
import { Field, InputType } from 'type-graphql'; | ||
import { Upload } from '../../external/interfaces/Upload'; | ||
|
||
@InputType() | ||
export class AssetInput { | ||
@Field(type => GraphQLUpload, { nullable: false }) | ||
public upload: Promise<Upload>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import S3 from 'aws-sdk/clients/s3'; | ||
import fileType from 'file-type'; | ||
import { ReadStream } from 'fs'; | ||
import { Service } from 'typedi'; | ||
import uuidv4 from 'uuid/v4'; | ||
import { Asset } from '../entities/Asset'; | ||
|
||
@Service() | ||
export class S3Storage { | ||
private bucket: string; | ||
private s3: S3; | ||
|
||
constructor() { | ||
if (!process.env.S3_BUCKET) { | ||
throw Error('S3_BUCKET not set'); | ||
} | ||
if (!process.env.S3_ACCESS_KEY_ID) { | ||
throw Error('S3_ACCESS_KEY_ID not set'); | ||
} | ||
if (!process.env.S3_SECRET_ACCESS_KEY) { | ||
throw Error('S3_SECRET_ACCESS_KEY not set'); | ||
} | ||
|
||
this.bucket = process.env.S3_BUCKET; | ||
this.s3 = new S3({ | ||
credentials: { | ||
accessKeyId: process.env.S3_ACCESS_KEY_ID, | ||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, | ||
}, | ||
}); | ||
} | ||
|
||
public async upload(fileName: string, readStream1: ReadStream): Promise<Partial<Asset>> { | ||
// TODO: Detect width/height of image types | ||
|
||
const readStream2 = await fileType.stream(readStream1); | ||
const { mime } = readStream2.fileType!; | ||
|
||
const handle = uuidv4(); | ||
const data = await this.s3 | ||
.upload({ | ||
Bucket: this.bucket, | ||
ACL: 'public-read', | ||
Body: readStream2, | ||
Key: handle, | ||
ContentType: mime, | ||
}) | ||
.promise(); | ||
const head = await this.head(handle); | ||
return { | ||
fileName, | ||
fileSize: head.ContentLength, | ||
handle, | ||
mimeType: mime, | ||
url: data.Location, | ||
}; | ||
} | ||
|
||
public async delete(fileName: string): Promise<boolean> { | ||
const data = await this.s3 | ||
.deleteObject({ | ||
Bucket: this.bucket, | ||
Key: fileName, | ||
}) | ||
.promise(); | ||
return data.DeleteMarker || false; | ||
} | ||
|
||
private async head(fileName: string) { | ||
return this.s3 | ||
.headObject({ | ||
Bucket: this.bucket, | ||
Key: fileName, | ||
}) | ||
.promise(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.