Преглед изворни кода

added database file handler and some training resolvers.

Tomi Cvetic пре 4 година
родитељ
комит
889e7c364e

+ 276 - 0
backend/database/generated/prisma-client/prisma-schema.ts

@@ -22,6 +22,10 @@ type AggregateExerciseInstance {
   count: Int!
 }
 
+type AggregateFile {
+  count: Int!
+}
+
 type AggregateFormat {
   count: Int!
 }
@@ -1303,6 +1307,251 @@ input ExerciseWhereUniqueInput {
   id: ID
 }
 
+type File {
+  id: ID!
+  createdAt: DateTime!
+  updatedAt: DateTime!
+  path: String!
+  mimetype: String!
+  user: User!
+  thumbnail: String
+  filename: String!
+  encoding: String!
+  size: Int!
+  comment: String
+}
+
+type FileConnection {
+  pageInfo: PageInfo!
+  edges: [FileEdge]!
+  aggregate: AggregateFile!
+}
+
+input FileCreateInput {
+  id: ID
+  path: String!
+  mimetype: String!
+  user: UserCreateOneInput!
+  thumbnail: String
+  filename: String!
+  encoding: String!
+  size: Int!
+  comment: String
+}
+
+type FileEdge {
+  node: File!
+  cursor: String!
+}
+
+enum FileOrderByInput {
+  id_ASC
+  id_DESC
+  createdAt_ASC
+  createdAt_DESC
+  updatedAt_ASC
+  updatedAt_DESC
+  path_ASC
+  path_DESC
+  mimetype_ASC
+  mimetype_DESC
+  thumbnail_ASC
+  thumbnail_DESC
+  filename_ASC
+  filename_DESC
+  encoding_ASC
+  encoding_DESC
+  size_ASC
+  size_DESC
+  comment_ASC
+  comment_DESC
+}
+
+type FilePreviousValues {
+  id: ID!
+  createdAt: DateTime!
+  updatedAt: DateTime!
+  path: String!
+  mimetype: String!
+  thumbnail: String
+  filename: String!
+  encoding: String!
+  size: Int!
+  comment: String
+}
+
+type FileSubscriptionPayload {
+  mutation: MutationType!
+  node: File
+  updatedFields: [String!]
+  previousValues: FilePreviousValues
+}
+
+input FileSubscriptionWhereInput {
+  mutation_in: [MutationType!]
+  updatedFields_contains: String
+  updatedFields_contains_every: [String!]
+  updatedFields_contains_some: [String!]
+  node: FileWhereInput
+  AND: [FileSubscriptionWhereInput!]
+  OR: [FileSubscriptionWhereInput!]
+  NOT: [FileSubscriptionWhereInput!]
+}
+
+input FileUpdateInput {
+  path: String
+  mimetype: String
+  user: UserUpdateOneRequiredInput
+  thumbnail: String
+  filename: String
+  encoding: String
+  size: Int
+  comment: String
+}
+
+input FileUpdateManyMutationInput {
+  path: String
+  mimetype: String
+  thumbnail: String
+  filename: String
+  encoding: String
+  size: Int
+  comment: String
+}
+
+input FileWhereInput {
+  id: ID
+  id_not: ID
+  id_in: [ID!]
+  id_not_in: [ID!]
+  id_lt: ID
+  id_lte: ID
+  id_gt: ID
+  id_gte: ID
+  id_contains: ID
+  id_not_contains: ID
+  id_starts_with: ID
+  id_not_starts_with: ID
+  id_ends_with: ID
+  id_not_ends_with: ID
+  createdAt: DateTime
+  createdAt_not: DateTime
+  createdAt_in: [DateTime!]
+  createdAt_not_in: [DateTime!]
+  createdAt_lt: DateTime
+  createdAt_lte: DateTime
+  createdAt_gt: DateTime
+  createdAt_gte: DateTime
+  updatedAt: DateTime
+  updatedAt_not: DateTime
+  updatedAt_in: [DateTime!]
+  updatedAt_not_in: [DateTime!]
+  updatedAt_lt: DateTime
+  updatedAt_lte: DateTime
+  updatedAt_gt: DateTime
+  updatedAt_gte: DateTime
+  path: String
+  path_not: String
+  path_in: [String!]
+  path_not_in: [String!]
+  path_lt: String
+  path_lte: String
+  path_gt: String
+  path_gte: String
+  path_contains: String
+  path_not_contains: String
+  path_starts_with: String
+  path_not_starts_with: String
+  path_ends_with: String
+  path_not_ends_with: String
+  mimetype: String
+  mimetype_not: String
+  mimetype_in: [String!]
+  mimetype_not_in: [String!]
+  mimetype_lt: String
+  mimetype_lte: String
+  mimetype_gt: String
+  mimetype_gte: String
+  mimetype_contains: String
+  mimetype_not_contains: String
+  mimetype_starts_with: String
+  mimetype_not_starts_with: String
+  mimetype_ends_with: String
+  mimetype_not_ends_with: String
+  user: UserWhereInput
+  thumbnail: String
+  thumbnail_not: String
+  thumbnail_in: [String!]
+  thumbnail_not_in: [String!]
+  thumbnail_lt: String
+  thumbnail_lte: String
+  thumbnail_gt: String
+  thumbnail_gte: String
+  thumbnail_contains: String
+  thumbnail_not_contains: String
+  thumbnail_starts_with: String
+  thumbnail_not_starts_with: String
+  thumbnail_ends_with: String
+  thumbnail_not_ends_with: String
+  filename: String
+  filename_not: String
+  filename_in: [String!]
+  filename_not_in: [String!]
+  filename_lt: String
+  filename_lte: String
+  filename_gt: String
+  filename_gte: String
+  filename_contains: String
+  filename_not_contains: String
+  filename_starts_with: String
+  filename_not_starts_with: String
+  filename_ends_with: String
+  filename_not_ends_with: String
+  encoding: String
+  encoding_not: String
+  encoding_in: [String!]
+  encoding_not_in: [String!]
+  encoding_lt: String
+  encoding_lte: String
+  encoding_gt: String
+  encoding_gte: String
+  encoding_contains: String
+  encoding_not_contains: String
+  encoding_starts_with: String
+  encoding_not_starts_with: String
+  encoding_ends_with: String
+  encoding_not_ends_with: String
+  size: Int
+  size_not: Int
+  size_in: [Int!]
+  size_not_in: [Int!]
+  size_lt: Int
+  size_lte: Int
+  size_gt: Int
+  size_gte: Int
+  comment: String
+  comment_not: String
+  comment_in: [String!]
+  comment_not_in: [String!]
+  comment_lt: String
+  comment_lte: String
+  comment_gt: String
+  comment_gte: String
+  comment_contains: String
+  comment_not_contains: String
+  comment_starts_with: String
+  comment_not_starts_with: String
+  comment_ends_with: String
+  comment_not_ends_with: String
+  AND: [FileWhereInput!]
+  OR: [FileWhereInput!]
+  NOT: [FileWhereInput!]
+}
+
+input FileWhereUniqueInput {
+  id: ID
+}
+
 type Format {
   id: ID!
   name: String!
@@ -1476,6 +1725,12 @@ type Mutation {
   upsertExerciseInstance(where: ExerciseInstanceWhereUniqueInput!, create: ExerciseInstanceCreateInput!, update: ExerciseInstanceUpdateInput!): ExerciseInstance!
   deleteExerciseInstance(where: ExerciseInstanceWhereUniqueInput!): ExerciseInstance
   deleteManyExerciseInstances(where: ExerciseInstanceWhereInput): BatchPayload!
+  createFile(data: FileCreateInput!): File!
+  updateFile(data: FileUpdateInput!, where: FileWhereUniqueInput!): File
+  updateManyFiles(data: FileUpdateManyMutationInput!, where: FileWhereInput): BatchPayload!
+  upsertFile(where: FileWhereUniqueInput!, create: FileCreateInput!, update: FileUpdateInput!): File!
+  deleteFile(where: FileWhereUniqueInput!): File
+  deleteManyFiles(where: FileWhereInput): BatchPayload!
   createFormat(data: FormatCreateInput!): Format!
   updateFormat(data: FormatUpdateInput!, where: FormatWhereUniqueInput!): Format
   updateManyFormats(data: FormatUpdateManyMutationInput!, where: FormatWhereInput): BatchPayload!
@@ -1552,6 +1807,9 @@ type Query {
   exerciseInstance(where: ExerciseInstanceWhereUniqueInput!): ExerciseInstance
   exerciseInstances(where: ExerciseInstanceWhereInput, orderBy: ExerciseInstanceOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [ExerciseInstance]!
   exerciseInstancesConnection(where: ExerciseInstanceWhereInput, orderBy: ExerciseInstanceOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): ExerciseInstanceConnection!
+  file(where: FileWhereUniqueInput!): File
+  files(where: FileWhereInput, orderBy: FileOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [File]!
+  filesConnection(where: FileWhereInput, orderBy: FileOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): FileConnection!
   format(where: FormatWhereUniqueInput!): Format
   formats(where: FormatWhereInput, orderBy: FormatOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Format]!
   formatsConnection(where: FormatWhereInput, orderBy: FormatOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): FormatConnection!
@@ -1840,6 +2098,7 @@ type Subscription {
   comment(where: CommentSubscriptionWhereInput): CommentSubscriptionPayload
   exercise(where: ExerciseSubscriptionWhereInput): ExerciseSubscriptionPayload
   exerciseInstance(where: ExerciseInstanceSubscriptionWhereInput): ExerciseInstanceSubscriptionPayload
+  file(where: FileSubscriptionWhereInput): FileSubscriptionPayload
   format(where: FormatSubscriptionWhereInput): FormatSubscriptionPayload
   rating(where: RatingSubscriptionWhereInput): RatingSubscriptionPayload
   track(where: TrackSubscriptionWhereInput): TrackSubscriptionPayload
@@ -2535,6 +2794,11 @@ input UserCreateManyInput {
   connect: [UserWhereUniqueInput!]
 }
 
+input UserCreateOneInput {
+  create: UserCreateInput
+  connect: UserWhereUniqueInput
+}
+
 input UserCreateOneWithoutCommentsInput {
   create: UserCreateWithoutCommentsInput
   connect: UserWhereUniqueInput
@@ -2782,6 +3046,13 @@ input UserUpdateManyWithWhereNestedInput {
   data: UserUpdateManyDataInput!
 }
 
+input UserUpdateOneRequiredInput {
+  create: UserCreateInput
+  update: UserUpdateDataInput
+  upsert: UserUpsertNestedInput
+  connect: UserWhereUniqueInput
+}
+
 input UserUpdateOneRequiredWithoutCommentsInput {
   create: UserCreateWithoutCommentsInput
   update: UserUpdateWithoutCommentsDataInput
@@ -2827,6 +3098,11 @@ input UserUpdateWithWhereUniqueNestedInput {
   data: UserUpdateDataInput!
 }
 
+input UserUpsertNestedInput {
+  update: UserUpdateDataInput!
+  create: UserCreateInput!
+}
+
 input UserUpsertWithoutCommentsInput {
   update: UserUpdateWithoutCommentsDataInput!
   create: UserCreateWithoutCommentsInput!

+ 543 - 11
backend/database/generated/prisma.graphql

@@ -1,5 +1,5 @@
 # source: http://prisma:4466
-# timestamp: Wed Apr 08 2020 18:07:43 GMT+0000 (Coordinated Universal Time)
+# timestamp: Fri Apr 10 2020 18:04:15 GMT+0000 (Coordinated Universal Time)
 
 type AggregateBlock {
   count: Int!
@@ -21,6 +21,10 @@ type AggregateExerciseInstance {
   count: Int!
 }
 
+type AggregateFile {
+  count: Int!
+}
+
 type AggregateFormat {
   count: Int!
 }
@@ -2114,6 +2118,507 @@ input ExerciseWhereUniqueInput {
   id: ID
 }
 
+type File implements Node {
+  id: ID!
+  createdAt: DateTime!
+  updatedAt: DateTime!
+  path: String!
+  mimetype: String!
+  user: User!
+  thumbnail: String
+  filename: String!
+  encoding: String!
+  size: Int!
+  comment: String
+}
+
+"""A connection to a list of items."""
+type FileConnection {
+  """Information to aid in pagination."""
+  pageInfo: PageInfo!
+
+  """A list of edges."""
+  edges: [FileEdge]!
+  aggregate: AggregateFile!
+}
+
+input FileCreateInput {
+  id: ID
+  path: String!
+  mimetype: String!
+  thumbnail: String
+  filename: String!
+  encoding: String!
+  size: Int!
+  comment: String
+  user: UserCreateOneInput!
+}
+
+"""An edge in a connection."""
+type FileEdge {
+  """The item at the end of the edge."""
+  node: File!
+
+  """A cursor for use in pagination."""
+  cursor: String!
+}
+
+enum FileOrderByInput {
+  id_ASC
+  id_DESC
+  createdAt_ASC
+  createdAt_DESC
+  updatedAt_ASC
+  updatedAt_DESC
+  path_ASC
+  path_DESC
+  mimetype_ASC
+  mimetype_DESC
+  thumbnail_ASC
+  thumbnail_DESC
+  filename_ASC
+  filename_DESC
+  encoding_ASC
+  encoding_DESC
+  size_ASC
+  size_DESC
+  comment_ASC
+  comment_DESC
+}
+
+type FilePreviousValues {
+  id: ID!
+  createdAt: DateTime!
+  updatedAt: DateTime!
+  path: String!
+  mimetype: String!
+  thumbnail: String
+  filename: String!
+  encoding: String!
+  size: Int!
+  comment: String
+}
+
+type FileSubscriptionPayload {
+  mutation: MutationType!
+  node: File
+  updatedFields: [String!]
+  previousValues: FilePreviousValues
+}
+
+input FileSubscriptionWhereInput {
+  """Logical AND on all given filters."""
+  AND: [FileSubscriptionWhereInput!]
+
+  """Logical OR on all given filters."""
+  OR: [FileSubscriptionWhereInput!]
+
+  """Logical NOT on all given filters combined by AND."""
+  NOT: [FileSubscriptionWhereInput!]
+
+  """The subscription event gets dispatched when it's listed in mutation_in"""
+  mutation_in: [MutationType!]
+
+  """
+  The subscription event gets only dispatched when one of the updated fields names is included in this list
+  """
+  updatedFields_contains: String
+
+  """
+  The subscription event gets only dispatched when all of the field names included in this list have been updated
+  """
+  updatedFields_contains_every: [String!]
+
+  """
+  The subscription event gets only dispatched when some of the field names included in this list have been updated
+  """
+  updatedFields_contains_some: [String!]
+  node: FileWhereInput
+}
+
+input FileUpdateInput {
+  path: String
+  mimetype: String
+  thumbnail: String
+  filename: String
+  encoding: String
+  size: Int
+  comment: String
+  user: UserUpdateOneRequiredInput
+}
+
+input FileUpdateManyMutationInput {
+  path: String
+  mimetype: String
+  thumbnail: String
+  filename: String
+  encoding: String
+  size: Int
+  comment: String
+}
+
+input FileWhereInput {
+  """Logical AND on all given filters."""
+  AND: [FileWhereInput!]
+
+  """Logical OR on all given filters."""
+  OR: [FileWhereInput!]
+
+  """Logical NOT on all given filters combined by AND."""
+  NOT: [FileWhereInput!]
+  id: ID
+
+  """All values that are not equal to given value."""
+  id_not: ID
+
+  """All values that are contained in given list."""
+  id_in: [ID!]
+
+  """All values that are not contained in given list."""
+  id_not_in: [ID!]
+
+  """All values less than the given value."""
+  id_lt: ID
+
+  """All values less than or equal the given value."""
+  id_lte: ID
+
+  """All values greater than the given value."""
+  id_gt: ID
+
+  """All values greater than or equal the given value."""
+  id_gte: ID
+
+  """All values containing the given string."""
+  id_contains: ID
+
+  """All values not containing the given string."""
+  id_not_contains: ID
+
+  """All values starting with the given string."""
+  id_starts_with: ID
+
+  """All values not starting with the given string."""
+  id_not_starts_with: ID
+
+  """All values ending with the given string."""
+  id_ends_with: ID
+
+  """All values not ending with the given string."""
+  id_not_ends_with: ID
+  createdAt: DateTime
+
+  """All values that are not equal to given value."""
+  createdAt_not: DateTime
+
+  """All values that are contained in given list."""
+  createdAt_in: [DateTime!]
+
+  """All values that are not contained in given list."""
+  createdAt_not_in: [DateTime!]
+
+  """All values less than the given value."""
+  createdAt_lt: DateTime
+
+  """All values less than or equal the given value."""
+  createdAt_lte: DateTime
+
+  """All values greater than the given value."""
+  createdAt_gt: DateTime
+
+  """All values greater than or equal the given value."""
+  createdAt_gte: DateTime
+  updatedAt: DateTime
+
+  """All values that are not equal to given value."""
+  updatedAt_not: DateTime
+
+  """All values that are contained in given list."""
+  updatedAt_in: [DateTime!]
+
+  """All values that are not contained in given list."""
+  updatedAt_not_in: [DateTime!]
+
+  """All values less than the given value."""
+  updatedAt_lt: DateTime
+
+  """All values less than or equal the given value."""
+  updatedAt_lte: DateTime
+
+  """All values greater than the given value."""
+  updatedAt_gt: DateTime
+
+  """All values greater than or equal the given value."""
+  updatedAt_gte: DateTime
+  path: String
+
+  """All values that are not equal to given value."""
+  path_not: String
+
+  """All values that are contained in given list."""
+  path_in: [String!]
+
+  """All values that are not contained in given list."""
+  path_not_in: [String!]
+
+  """All values less than the given value."""
+  path_lt: String
+
+  """All values less than or equal the given value."""
+  path_lte: String
+
+  """All values greater than the given value."""
+  path_gt: String
+
+  """All values greater than or equal the given value."""
+  path_gte: String
+
+  """All values containing the given string."""
+  path_contains: String
+
+  """All values not containing the given string."""
+  path_not_contains: String
+
+  """All values starting with the given string."""
+  path_starts_with: String
+
+  """All values not starting with the given string."""
+  path_not_starts_with: String
+
+  """All values ending with the given string."""
+  path_ends_with: String
+
+  """All values not ending with the given string."""
+  path_not_ends_with: String
+  mimetype: String
+
+  """All values that are not equal to given value."""
+  mimetype_not: String
+
+  """All values that are contained in given list."""
+  mimetype_in: [String!]
+
+  """All values that are not contained in given list."""
+  mimetype_not_in: [String!]
+
+  """All values less than the given value."""
+  mimetype_lt: String
+
+  """All values less than or equal the given value."""
+  mimetype_lte: String
+
+  """All values greater than the given value."""
+  mimetype_gt: String
+
+  """All values greater than or equal the given value."""
+  mimetype_gte: String
+
+  """All values containing the given string."""
+  mimetype_contains: String
+
+  """All values not containing the given string."""
+  mimetype_not_contains: String
+
+  """All values starting with the given string."""
+  mimetype_starts_with: String
+
+  """All values not starting with the given string."""
+  mimetype_not_starts_with: String
+
+  """All values ending with the given string."""
+  mimetype_ends_with: String
+
+  """All values not ending with the given string."""
+  mimetype_not_ends_with: String
+  thumbnail: String
+
+  """All values that are not equal to given value."""
+  thumbnail_not: String
+
+  """All values that are contained in given list."""
+  thumbnail_in: [String!]
+
+  """All values that are not contained in given list."""
+  thumbnail_not_in: [String!]
+
+  """All values less than the given value."""
+  thumbnail_lt: String
+
+  """All values less than or equal the given value."""
+  thumbnail_lte: String
+
+  """All values greater than the given value."""
+  thumbnail_gt: String
+
+  """All values greater than or equal the given value."""
+  thumbnail_gte: String
+
+  """All values containing the given string."""
+  thumbnail_contains: String
+
+  """All values not containing the given string."""
+  thumbnail_not_contains: String
+
+  """All values starting with the given string."""
+  thumbnail_starts_with: String
+
+  """All values not starting with the given string."""
+  thumbnail_not_starts_with: String
+
+  """All values ending with the given string."""
+  thumbnail_ends_with: String
+
+  """All values not ending with the given string."""
+  thumbnail_not_ends_with: String
+  filename: String
+
+  """All values that are not equal to given value."""
+  filename_not: String
+
+  """All values that are contained in given list."""
+  filename_in: [String!]
+
+  """All values that are not contained in given list."""
+  filename_not_in: [String!]
+
+  """All values less than the given value."""
+  filename_lt: String
+
+  """All values less than or equal the given value."""
+  filename_lte: String
+
+  """All values greater than the given value."""
+  filename_gt: String
+
+  """All values greater than or equal the given value."""
+  filename_gte: String
+
+  """All values containing the given string."""
+  filename_contains: String
+
+  """All values not containing the given string."""
+  filename_not_contains: String
+
+  """All values starting with the given string."""
+  filename_starts_with: String
+
+  """All values not starting with the given string."""
+  filename_not_starts_with: String
+
+  """All values ending with the given string."""
+  filename_ends_with: String
+
+  """All values not ending with the given string."""
+  filename_not_ends_with: String
+  encoding: String
+
+  """All values that are not equal to given value."""
+  encoding_not: String
+
+  """All values that are contained in given list."""
+  encoding_in: [String!]
+
+  """All values that are not contained in given list."""
+  encoding_not_in: [String!]
+
+  """All values less than the given value."""
+  encoding_lt: String
+
+  """All values less than or equal the given value."""
+  encoding_lte: String
+
+  """All values greater than the given value."""
+  encoding_gt: String
+
+  """All values greater than or equal the given value."""
+  encoding_gte: String
+
+  """All values containing the given string."""
+  encoding_contains: String
+
+  """All values not containing the given string."""
+  encoding_not_contains: String
+
+  """All values starting with the given string."""
+  encoding_starts_with: String
+
+  """All values not starting with the given string."""
+  encoding_not_starts_with: String
+
+  """All values ending with the given string."""
+  encoding_ends_with: String
+
+  """All values not ending with the given string."""
+  encoding_not_ends_with: String
+  size: Int
+
+  """All values that are not equal to given value."""
+  size_not: Int
+
+  """All values that are contained in given list."""
+  size_in: [Int!]
+
+  """All values that are not contained in given list."""
+  size_not_in: [Int!]
+
+  """All values less than the given value."""
+  size_lt: Int
+
+  """All values less than or equal the given value."""
+  size_lte: Int
+
+  """All values greater than the given value."""
+  size_gt: Int
+
+  """All values greater than or equal the given value."""
+  size_gte: Int
+  comment: String
+
+  """All values that are not equal to given value."""
+  comment_not: String
+
+  """All values that are contained in given list."""
+  comment_in: [String!]
+
+  """All values that are not contained in given list."""
+  comment_not_in: [String!]
+
+  """All values less than the given value."""
+  comment_lt: String
+
+  """All values less than or equal the given value."""
+  comment_lte: String
+
+  """All values greater than the given value."""
+  comment_gt: String
+
+  """All values greater than or equal the given value."""
+  comment_gte: String
+
+  """All values containing the given string."""
+  comment_contains: String
+
+  """All values not containing the given string."""
+  comment_not_contains: String
+
+  """All values starting with the given string."""
+  comment_starts_with: String
+
+  """All values not starting with the given string."""
+  comment_not_starts_with: String
+
+  """All values ending with the given string."""
+  comment_ends_with: String
+
+  """All values not ending with the given string."""
+  comment_not_ends_with: String
+  user: UserWhereInput
+}
+
+input FileWhereUniqueInput {
+  id: ID
+}
+
 type Format implements Node {
   id: ID!
   name: String!
@@ -2371,67 +2876,73 @@ Long can represent values between -(2^63) and 2^63 - 1.
 scalar Long
 
 type Mutation {
+  createFile(data: FileCreateInput!): File!
   createTraining(data: TrainingCreateInput!): Training!
   createBlock(data: BlockCreateInput!): Block!
   createBlockInstance(data: BlockInstanceCreateInput!): BlockInstance!
   createComment(data: CommentCreateInput!): Comment!
-  createUser(data: UserCreateInput!): User!
   createTrainingType(data: TrainingTypeCreateInput!): TrainingType!
+  createUser(data: UserCreateInput!): User!
   createTrack(data: TrackCreateInput!): Track!
   createExercise(data: ExerciseCreateInput!): Exercise!
   createFormat(data: FormatCreateInput!): Format!
   createExerciseInstance(data: ExerciseInstanceCreateInput!): ExerciseInstance!
   createRating(data: RatingCreateInput!): Rating!
+  updateFile(data: FileUpdateInput!, where: FileWhereUniqueInput!): File
   updateTraining(data: TrainingUpdateInput!, where: TrainingWhereUniqueInput!): Training
   updateBlock(data: BlockUpdateInput!, where: BlockWhereUniqueInput!): Block
   updateBlockInstance(data: BlockInstanceUpdateInput!, where: BlockInstanceWhereUniqueInput!): BlockInstance
   updateComment(data: CommentUpdateInput!, where: CommentWhereUniqueInput!): Comment
-  updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User
   updateTrainingType(data: TrainingTypeUpdateInput!, where: TrainingTypeWhereUniqueInput!): TrainingType
+  updateUser(data: UserUpdateInput!, where: UserWhereUniqueInput!): User
   updateTrack(data: TrackUpdateInput!, where: TrackWhereUniqueInput!): Track
   updateExercise(data: ExerciseUpdateInput!, where: ExerciseWhereUniqueInput!): Exercise
   updateFormat(data: FormatUpdateInput!, where: FormatWhereUniqueInput!): Format
   updateExerciseInstance(data: ExerciseInstanceUpdateInput!, where: ExerciseInstanceWhereUniqueInput!): ExerciseInstance
   updateRating(data: RatingUpdateInput!, where: RatingWhereUniqueInput!): Rating
+  deleteFile(where: FileWhereUniqueInput!): File
   deleteTraining(where: TrainingWhereUniqueInput!): Training
   deleteBlock(where: BlockWhereUniqueInput!): Block
   deleteBlockInstance(where: BlockInstanceWhereUniqueInput!): BlockInstance
   deleteComment(where: CommentWhereUniqueInput!): Comment
-  deleteUser(where: UserWhereUniqueInput!): User
   deleteTrainingType(where: TrainingTypeWhereUniqueInput!): TrainingType
+  deleteUser(where: UserWhereUniqueInput!): User
   deleteTrack(where: TrackWhereUniqueInput!): Track
   deleteExercise(where: ExerciseWhereUniqueInput!): Exercise
   deleteFormat(where: FormatWhereUniqueInput!): Format
   deleteExerciseInstance(where: ExerciseInstanceWhereUniqueInput!): ExerciseInstance
   deleteRating(where: RatingWhereUniqueInput!): Rating
+  upsertFile(where: FileWhereUniqueInput!, create: FileCreateInput!, update: FileUpdateInput!): File!
   upsertTraining(where: TrainingWhereUniqueInput!, create: TrainingCreateInput!, update: TrainingUpdateInput!): Training!
   upsertBlock(where: BlockWhereUniqueInput!, create: BlockCreateInput!, update: BlockUpdateInput!): Block!
   upsertBlockInstance(where: BlockInstanceWhereUniqueInput!, create: BlockInstanceCreateInput!, update: BlockInstanceUpdateInput!): BlockInstance!
   upsertComment(where: CommentWhereUniqueInput!, create: CommentCreateInput!, update: CommentUpdateInput!): Comment!
-  upsertUser(where: UserWhereUniqueInput!, create: UserCreateInput!, update: UserUpdateInput!): User!
   upsertTrainingType(where: TrainingTypeWhereUniqueInput!, create: TrainingTypeCreateInput!, update: TrainingTypeUpdateInput!): TrainingType!
+  upsertUser(where: UserWhereUniqueInput!, create: UserCreateInput!, update: UserUpdateInput!): User!
   upsertTrack(where: TrackWhereUniqueInput!, create: TrackCreateInput!, update: TrackUpdateInput!): Track!
   upsertExercise(where: ExerciseWhereUniqueInput!, create: ExerciseCreateInput!, update: ExerciseUpdateInput!): Exercise!
   upsertFormat(where: FormatWhereUniqueInput!, create: FormatCreateInput!, update: FormatUpdateInput!): Format!
   upsertExerciseInstance(where: ExerciseInstanceWhereUniqueInput!, create: ExerciseInstanceCreateInput!, update: ExerciseInstanceUpdateInput!): ExerciseInstance!
   upsertRating(where: RatingWhereUniqueInput!, create: RatingCreateInput!, update: RatingUpdateInput!): Rating!
+  updateManyFiles(data: FileUpdateManyMutationInput!, where: FileWhereInput): BatchPayload!
   updateManyTrainings(data: TrainingUpdateManyMutationInput!, where: TrainingWhereInput): BatchPayload!
   updateManyBlocks(data: BlockUpdateManyMutationInput!, where: BlockWhereInput): BatchPayload!
   updateManyBlockInstances(data: BlockInstanceUpdateManyMutationInput!, where: BlockInstanceWhereInput): BatchPayload!
   updateManyComments(data: CommentUpdateManyMutationInput!, where: CommentWhereInput): BatchPayload!
-  updateManyUsers(data: UserUpdateManyMutationInput!, where: UserWhereInput): BatchPayload!
   updateManyTrainingTypes(data: TrainingTypeUpdateManyMutationInput!, where: TrainingTypeWhereInput): BatchPayload!
+  updateManyUsers(data: UserUpdateManyMutationInput!, where: UserWhereInput): BatchPayload!
   updateManyTracks(data: TrackUpdateManyMutationInput!, where: TrackWhereInput): BatchPayload!
   updateManyExercises(data: ExerciseUpdateManyMutationInput!, where: ExerciseWhereInput): BatchPayload!
   updateManyFormats(data: FormatUpdateManyMutationInput!, where: FormatWhereInput): BatchPayload!
   updateManyExerciseInstances(data: ExerciseInstanceUpdateManyMutationInput!, where: ExerciseInstanceWhereInput): BatchPayload!
   updateManyRatings(data: RatingUpdateManyMutationInput!, where: RatingWhereInput): BatchPayload!
+  deleteManyFiles(where: FileWhereInput): BatchPayload!
   deleteManyTrainings(where: TrainingWhereInput): BatchPayload!
   deleteManyBlocks(where: BlockWhereInput): BatchPayload!
   deleteManyBlockInstances(where: BlockInstanceWhereInput): BatchPayload!
   deleteManyComments(where: CommentWhereInput): BatchPayload!
-  deleteManyUsers(where: UserWhereInput): BatchPayload!
   deleteManyTrainingTypes(where: TrainingTypeWhereInput): BatchPayload!
+  deleteManyUsers(where: UserWhereInput): BatchPayload!
   deleteManyTracks(where: TrackWhereInput): BatchPayload!
   deleteManyExercises(where: ExerciseWhereInput): BatchPayload!
   deleteManyFormats(where: FormatWhereInput): BatchPayload!
@@ -2472,34 +2983,37 @@ enum Permission {
 }
 
 type Query {
+  files(where: FileWhereInput, orderBy: FileOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [File]!
   trainings(where: TrainingWhereInput, orderBy: TrainingOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Training]!
   blocks(where: BlockWhereInput, orderBy: BlockOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Block]!
   blockInstances(where: BlockInstanceWhereInput, orderBy: BlockInstanceOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [BlockInstance]!
   comments(where: CommentWhereInput, orderBy: CommentOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Comment]!
-  users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
   trainingTypes(where: TrainingTypeWhereInput, orderBy: TrainingTypeOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [TrainingType]!
+  users(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [User]!
   tracks(where: TrackWhereInput, orderBy: TrackOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Track]!
   exercises(where: ExerciseWhereInput, orderBy: ExerciseOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Exercise]!
   formats(where: FormatWhereInput, orderBy: FormatOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Format]!
   exerciseInstances(where: ExerciseInstanceWhereInput, orderBy: ExerciseInstanceOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [ExerciseInstance]!
   ratings(where: RatingWhereInput, orderBy: RatingOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): [Rating]!
+  file(where: FileWhereUniqueInput!): File
   training(where: TrainingWhereUniqueInput!): Training
   block(where: BlockWhereUniqueInput!): Block
   blockInstance(where: BlockInstanceWhereUniqueInput!): BlockInstance
   comment(where: CommentWhereUniqueInput!): Comment
-  user(where: UserWhereUniqueInput!): User
   trainingType(where: TrainingTypeWhereUniqueInput!): TrainingType
+  user(where: UserWhereUniqueInput!): User
   track(where: TrackWhereUniqueInput!): Track
   exercise(where: ExerciseWhereUniqueInput!): Exercise
   format(where: FormatWhereUniqueInput!): Format
   exerciseInstance(where: ExerciseInstanceWhereUniqueInput!): ExerciseInstance
   rating(where: RatingWhereUniqueInput!): Rating
+  filesConnection(where: FileWhereInput, orderBy: FileOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): FileConnection!
   trainingsConnection(where: TrainingWhereInput, orderBy: TrainingOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): TrainingConnection!
   blocksConnection(where: BlockWhereInput, orderBy: BlockOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): BlockConnection!
   blockInstancesConnection(where: BlockInstanceWhereInput, orderBy: BlockInstanceOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): BlockInstanceConnection!
   commentsConnection(where: CommentWhereInput, orderBy: CommentOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): CommentConnection!
-  usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection!
   trainingTypesConnection(where: TrainingTypeWhereInput, orderBy: TrainingTypeOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): TrainingTypeConnection!
+  usersConnection(where: UserWhereInput, orderBy: UserOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): UserConnection!
   tracksConnection(where: TrackWhereInput, orderBy: TrackOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): TrackConnection!
   exercisesConnection(where: ExerciseWhereInput, orderBy: ExerciseOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): ExerciseConnection!
   formatsConnection(where: FormatWhereInput, orderBy: FormatOrderByInput, skip: Int, after: String, before: String, first: Int, last: Int): FormatConnection!
@@ -2972,12 +3486,13 @@ input RatingWhereUniqueInput {
 }
 
 type Subscription {
+  file(where: FileSubscriptionWhereInput): FileSubscriptionPayload
   training(where: TrainingSubscriptionWhereInput): TrainingSubscriptionPayload
   block(where: BlockSubscriptionWhereInput): BlockSubscriptionPayload
   blockInstance(where: BlockInstanceSubscriptionWhereInput): BlockInstanceSubscriptionPayload
   comment(where: CommentSubscriptionWhereInput): CommentSubscriptionPayload
-  user(where: UserSubscriptionWhereInput): UserSubscriptionPayload
   trainingType(where: TrainingTypeSubscriptionWhereInput): TrainingTypeSubscriptionPayload
+  user(where: UserSubscriptionWhereInput): UserSubscriptionPayload
   track(where: TrackSubscriptionWhereInput): TrackSubscriptionPayload
   exercise(where: ExerciseSubscriptionWhereInput): ExerciseSubscriptionPayload
   format(where: FormatSubscriptionWhereInput): FormatSubscriptionPayload
@@ -4213,6 +4728,11 @@ input UserCreateManyInput {
   connect: [UserWhereUniqueInput!]
 }
 
+input UserCreateOneInput {
+  create: UserCreateInput
+  connect: UserWhereUniqueInput
+}
+
 input UserCreateOneWithoutCommentsInput {
   create: UserCreateWithoutCommentsInput
   connect: UserWhereUniqueInput
@@ -4646,6 +5166,13 @@ input UserUpdateManyWithWhereNestedInput {
   data: UserUpdateManyDataInput!
 }
 
+input UserUpdateOneRequiredInput {
+  create: UserCreateInput
+  connect: UserWhereUniqueInput
+  update: UserUpdateDataInput
+  upsert: UserUpsertNestedInput
+}
+
 input UserUpdateOneRequiredWithoutCommentsInput {
   create: UserCreateWithoutCommentsInput
   connect: UserWhereUniqueInput
@@ -4691,6 +5218,11 @@ input UserUpdateWithWhereUniqueNestedInput {
   data: UserUpdateDataInput!
 }
 
+input UserUpsertNestedInput {
+  update: UserUpdateDataInput!
+  create: UserCreateInput!
+}
+
 input UserUpsertWithoutCommentsInput {
   update: UserUpdateWithoutCommentsDataInput!
   create: UserCreateWithoutCommentsInput!

+ 14 - 0
backend/datamodel.prisma

@@ -17,6 +17,20 @@ enum Permission {
     INSTRUCTOR
 }
 
+type File {
+    id: ID! @id
+    createdAt: DateTime! @createdAt
+    updatedAt: DateTime! @updatedAt
+    path: String!
+    mimetype: String!
+    user: User!
+    thumbnail: String
+    filename: String!
+    encoding: String!
+    size: Int!
+    comment: String
+}
+
 type Training {
     id: ID! @id
     title: String!

+ 17 - 0
backend/schema.graphql

@@ -1,6 +1,19 @@
 # import * from './database/generated/prisma.graphql'
 
+scalar Upload
+
+type FsFile {
+  filename: String!
+  path: String!
+  size: Int!
+  ctime: DateTime!
+  mtime: DateTime!
+}
+
 type Query {
+  # File module
+  fsFiles(directory: String!): [FsFile!]!
+  files: [File!]!
   currentUser: User!
   user(where: UserWhereUniqueInput!): User
   users(
@@ -22,6 +35,7 @@ type Query {
     first: Int
     last: Int
   ): [Training!]!
+  publishedTrainings: [Training!]!
   trainingType(where: TrainingTypeWhereUniqueInput!): TrainingType
   trainingTypes(
     where: TrainingTypeWhereInput
@@ -65,6 +79,9 @@ type Query {
 }
 
 type Mutation {
+  # File module
+  uploadFile(file: Upload!, comment: String): File!
+
   createUser(data: UserCreateInput!): User!
   updateUser(email: String!, data: UserUpdateInput!): User
   deleteUser(email: String!): User

+ 3 - 0
backend/src/file/constants.ts

@@ -0,0 +1,3 @@
+export const tmpDir = 'tmp'
+export const uploadDir = 'upload_files'
+export const thumbnails = 'thumbnails'

+ 3 - 0
backend/src/file/index.ts

@@ -0,0 +1,3 @@
+import { resolvers } from './resolvers'
+
+export default { resolvers }

+ 118 - 0
backend/src/file/resolvers.ts

@@ -0,0 +1,118 @@
+import { IResolvers } from 'apollo-server-express'
+import fs from 'fs'
+import randombytes from 'randombytes'
+import sharp from 'sharp'
+import { tmpDir, uploadDir, thumbnails } from './constants'
+import user from '../user'
+
+export const resolvers: IResolvers = {
+  Query: {
+    fsFiles: async (parent, { directory }, context, info) => {
+      user.checkPermission(context, 'ADMIN')
+      const data = await fsFiles(directory)
+      return data
+    },
+    files: (parent, args, context, info) => {
+      user.checkPermission(context, 'ADMIN')
+      return context.db.query.files(args, info)
+    }
+  },
+  Mutation: {
+    uploadFile: async (parent, { comment, file }, context, info) => {
+      user.checkPermission(context, 'ADMIN')
+      const fileInfo = await uploadFile(file)
+
+      return context.db.mutation.createFile(
+        {
+          data: {
+            ...fileInfo,
+            comment,
+            user: { connect: { id: context.req.userId } }
+          }
+        },
+        info
+      )
+    }
+  }
+}
+
+async function fsFiles(directory: string) {
+  const fileList = await fs.promises.readdir(directory)
+  return Promise.all(
+    fileList.map(async filename => {
+      const path = `${directory}/${filename}`
+      const { size, ctime, mtime } = await fs.promises.stat(path)
+      return {
+        filename,
+        path,
+        size,
+        ctime,
+        mtime
+      }
+    })
+  )
+}
+
+async function uploadFile(file: any) {
+  const { createReadStream, filename, mimetype, encoding } = await file
+  const stream = createReadStream()
+
+  const fsFilename = randombytes(16).toString('hex')
+  const tmpPath = `${tmpDir}/${fsFilename}`
+  const path = `${uploadDir}/${fsFilename}`
+  const thumbnailPath = `${thumbnails}/${fsFilename}`
+  await new Promise((resolve, reject) => {
+    const file = fs.createWriteStream(tmpPath)
+    file.on('finish', resolve)
+    file.on('error', error => {
+      fs.unlink(tmpPath, () => {
+        reject(error)
+      })
+    })
+    stream.on('error', (error: any) => file.destroy(error))
+    stream.pipe(file)
+  })
+
+  if (mimetype.startsWith('image/')) {
+    try {
+      await processImage(tmpPath, path)
+      await createThumbnail(tmpPath, thumbnailPath)
+      await fs.promises.unlink(tmpPath)
+    } catch (error) {
+      try {
+        await fs.promises.unlink(tmpPath)
+        await fs.promises.unlink(path)
+      } catch (ignore) {}
+      throw error
+    }
+  }
+
+  const { size } = await fs.promises.stat(path)
+
+  return {
+    filename,
+    path,
+    mimetype,
+    encoding,
+    size
+  }
+}
+
+function processImage(tmpFile: string, outputFile: string) {
+  return sharp(tmpFile)
+    .resize(1600, 1600, {
+      fit: sharp.fit.inside,
+      withoutEnlargement: true
+    })
+    .jpeg()
+    .toFile(outputFile)
+}
+
+function createThumbnail(tmpFile: string, thumbnail: string) {
+  return sharp(tmpFile)
+    .resize(200, 200, {
+      fit: sharp.fit.inside
+    })
+    .jpeg()
+    .toFile(thumbnail)
+}

+ 12 - 2
backend/src/training/resolvers.ts

@@ -12,10 +12,16 @@ export const resolvers: IResolvers = {
       return context.db.query.training({ where: args }, info)
     },
     trainings: async (parent, args, context, info) => {
-      checkPermission(context)
-      console.log(info)
+      checkPermission(context, ['INSTRUCTOR', 'ADMIN'])
       return context.db.query.trainings({}, info)
     },
+    publishedTrainings: async (parent, args, context, info) => {
+      checkPermission(context)
+      return context.db.query.trainings(
+        { where: { published: true }, orderBy: 'trainingDate_DESC', first: 20 },
+        info
+      )
+    },
     trainingTypes: async (parent, args, context, info) => {
       checkPermission(context)
       return context.db.query.trainingTypes({}, info)
@@ -27,6 +33,10 @@ export const resolvers: IResolvers = {
     formats: async (parent, args, context, info) => {
       checkPermission(context)
       return context.db.query.formats({}, info)
+    },
+    exercises: async (parent, args, context, info) => {
+      checkPermission(context)
+      return context.db.query.exercises({}, info)
     }
   },