diff --git a/api/v1/activity.go b/api/v1/activity.go index 2db2c6e..c9fac7a 100644 --- a/api/v1/activity.go +++ b/api/v1/activity.go @@ -2,7 +2,9 @@ package v1 import ( "context" + "time" + log "github.com/sirupsen/logrus" "reichard.io/antholume/database" ) @@ -72,3 +74,78 @@ func (s *Server) GetActivity(ctx context.Context, request GetActivityRequestObje } return GetActivity200JSONResponse(response), nil } + +// POST /activity +func (s *Server) CreateActivity(ctx context.Context, request CreateActivityRequestObject) (CreateActivityResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return CreateActivity401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + if request.Body == nil { + return CreateActivity400JSONResponse{Code: 400, Message: "Request body is required"}, nil + } + + tx, err := s.db.DB.Begin() + if err != nil { + log.Error("Transaction Begin DB Error:", err) + return CreateActivity500JSONResponse{Code: 500, Message: "Database error"}, nil + } + committed := false + defer func() { + if committed { + return + } + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Debug("Transaction Rollback DB Error:", rollbackErr) + } + }() + + qtx := s.db.Queries.WithTx(tx) + + allDocumentsMap := make(map[string]struct{}) + for _, item := range request.Body.Activity { + allDocumentsMap[item.DocumentId] = struct{}{} + } + + for documentID := range allDocumentsMap { + if _, err := qtx.UpsertDocument(ctx, database.UpsertDocumentParams{ID: documentID}); err != nil { + log.Error("UpsertDocument DB Error:", err) + return CreateActivity400JSONResponse{Code: 400, Message: "Invalid document"}, nil + } + } + + if _, err := qtx.UpsertDevice(ctx, database.UpsertDeviceParams{ + ID: request.Body.DeviceId, + UserID: auth.UserName, + DeviceName: request.Body.DeviceName, + LastSynced: time.Now().UTC().Format(time.RFC3339), + }); err != nil { + log.Error("UpsertDevice DB Error:", err) + return CreateActivity400JSONResponse{Code: 400, Message: "Invalid device"}, nil + } + + for _, item := range request.Body.Activity { + if _, err := qtx.AddActivity(ctx, database.AddActivityParams{ + UserID: auth.UserName, + DocumentID: item.DocumentId, + DeviceID: request.Body.DeviceId, + StartTime: time.Unix(item.StartTime, 0).UTC().Format(time.RFC3339), + Duration: item.Duration, + StartPercentage: float64(item.Page) / float64(item.Pages), + EndPercentage: float64(item.Page+1) / float64(item.Pages), + }); err != nil { + log.Error("AddActivity DB Error:", err) + return CreateActivity400JSONResponse{Code: 400, Message: "Invalid activity"}, nil + } + } + + if err := tx.Commit(); err != nil { + log.Error("Transaction Commit DB Error:", err) + return CreateActivity500JSONResponse{Code: 500, Message: "Database error"}, nil + } + committed = true + + response := CreateActivityResponse{Added: int64(len(request.Body.Activity))} + return CreateActivity200JSONResponse(response), nil +} diff --git a/api/v1/api.gen.go b/api/v1/api.gen.go index 4f7e8af..1e32df5 100644 --- a/api/v1/api.gen.go +++ b/api/v1/api.gen.go @@ -164,6 +164,27 @@ type ActivityResponse struct { // BackupType defines model for BackupType. type BackupType string +// CreateActivityItem defines model for CreateActivityItem. +type CreateActivityItem struct { + DocumentId string `json:"document_id"` + Duration int64 `json:"duration"` + Page int64 `json:"page"` + Pages int64 `json:"pages"` + StartTime int64 `json:"start_time"` +} + +// CreateActivityRequest defines model for CreateActivityRequest. +type CreateActivityRequest struct { + Activity []CreateActivityItem `json:"activity"` + DeviceId string `json:"device_id"` + DeviceName string `json:"device_name"` +} + +// CreateActivityResponse defines model for CreateActivityResponse. +type CreateActivityResponse struct { + Added int64 `json:"added"` +} + // DatabaseInfo defines model for DatabaseInfo. type DatabaseInfo struct { ActivitySize int64 `json:"activity_size"` @@ -335,9 +356,11 @@ type OperationType string type Progress struct { Author *string `json:"author,omitempty"` CreatedAt *time.Time `json:"created_at,omitempty"` + DeviceId *string `json:"device_id,omitempty"` DeviceName *string `json:"device_name,omitempty"` DocumentId *string `json:"document_id,omitempty"` Percentage *float64 `json:"percentage,omitempty"` + Progress *string `json:"progress,omitempty"` Title *string `json:"title,omitempty"` UserId *string `json:"user_id,omitempty"` } @@ -388,6 +411,21 @@ type StreaksResponse struct { Streaks []UserStreak `json:"streaks"` } +// UpdateProgressRequest defines model for UpdateProgressRequest. +type UpdateProgressRequest struct { + DeviceId string `json:"device_id"` + DeviceName string `json:"device_name"` + DocumentId string `json:"document_id"` + Percentage float64 `json:"percentage"` + Progress string `json:"progress"` +} + +// UpdateProgressResponse defines model for UpdateProgressResponse. +type UpdateProgressResponse struct { + DocumentId string `json:"document_id"` + Timestamp time.Time `json:"timestamp"` +} + // UpdateSettingsRequest defines model for UpdateSettingsRequest. type UpdateSettingsRequest struct { NewPassword *string `json:"new_password,omitempty"` @@ -533,6 +571,9 @@ type PostSearchFormdataBody struct { Title string `form:"title" json:"title"` } +// CreateActivityJSONRequestBody defines body for CreateActivity for application/json ContentType. +type CreateActivityJSONRequestBody = CreateActivityRequest + // PostAdminActionMultipartRequestBody defines body for PostAdminAction for multipart/form-data ContentType. type PostAdminActionMultipartRequestBody PostAdminActionMultipartBody @@ -557,6 +598,9 @@ type EditDocumentJSONRequestBody EditDocumentJSONBody // UploadDocumentCoverMultipartRequestBody defines body for UploadDocumentCover for multipart/form-data ContentType. type UploadDocumentCoverMultipartRequestBody UploadDocumentCoverMultipartBody +// UpdateProgressJSONRequestBody defines body for UpdateProgress for application/json ContentType. +type UpdateProgressJSONRequestBody = UpdateProgressRequest + // PostSearchFormdataRequestBody defines body for PostSearch for application/x-www-form-urlencoded ContentType. type PostSearchFormdataRequestBody PostSearchFormdataBody @@ -568,6 +612,9 @@ type ServerInterface interface { // Get activity data // (GET /activity) GetActivity(w http.ResponseWriter, r *http.Request, params GetActivityParams) + // Create activity records + // (POST /activity) + CreateActivity(w http.ResponseWriter, r *http.Request) // Get admin page data // (GET /admin) GetAdmin(w http.ResponseWriter, r *http.Request) @@ -643,6 +690,9 @@ type ServerInterface interface { // List progress records // (GET /progress) GetProgressList(w http.ResponseWriter, r *http.Request, params GetProgressListParams) + // Update document progress + // (PUT /progress) + UpdateProgress(w http.ResponseWriter, r *http.Request) // Get document progress // (GET /progress/{id}) GetProgress(w http.ResponseWriter, r *http.Request, id string) @@ -726,6 +776,26 @@ func (siw *ServerInterfaceWrapper) GetActivity(w http.ResponseWriter, r *http.Re handler.ServeHTTP(w, r) } +// CreateActivity operation middleware +func (siw *ServerInterfaceWrapper) CreateActivity(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CreateActivity(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetAdmin operation middleware func (siw *ServerInterfaceWrapper) GetAdmin(w http.ResponseWriter, r *http.Request) { @@ -1371,6 +1441,26 @@ func (siw *ServerInterfaceWrapper) GetProgressList(w http.ResponseWriter, r *htt handler.ServeHTTP(w, r) } +// UpdateProgress operation middleware +func (siw *ServerInterfaceWrapper) UpdateProgress(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.UpdateProgress(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetProgress operation middleware func (siw *ServerInterfaceWrapper) GetProgress(w http.ResponseWriter, r *http.Request) { @@ -1638,6 +1728,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H } m.HandleFunc("GET "+options.BaseURL+"/activity", wrapper.GetActivity) + m.HandleFunc("POST "+options.BaseURL+"/activity", wrapper.CreateActivity) m.HandleFunc("GET "+options.BaseURL+"/admin", wrapper.GetAdmin) m.HandleFunc("POST "+options.BaseURL+"/admin", wrapper.PostAdminAction) m.HandleFunc("GET "+options.BaseURL+"/admin/import", wrapper.GetImportDirectory) @@ -1663,6 +1754,7 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc("GET "+options.BaseURL+"/home/streaks", wrapper.GetStreaks) m.HandleFunc("GET "+options.BaseURL+"/info", wrapper.GetInfo) m.HandleFunc("GET "+options.BaseURL+"/progress", wrapper.GetProgressList) + m.HandleFunc("PUT "+options.BaseURL+"/progress", wrapper.UpdateProgress) m.HandleFunc("GET "+options.BaseURL+"/progress/{id}", wrapper.GetProgress) m.HandleFunc("GET "+options.BaseURL+"/search", wrapper.GetSearch) m.HandleFunc("POST "+options.BaseURL+"/search", wrapper.PostSearch) @@ -1707,6 +1799,50 @@ func (response GetActivity500JSONResponse) VisitGetActivityResponse(w http.Respo return json.NewEncoder(w).Encode(response) } +type CreateActivityRequestObject struct { + Body *CreateActivityJSONRequestBody +} + +type CreateActivityResponseObject interface { + VisitCreateActivityResponse(w http.ResponseWriter) error +} + +type CreateActivity200JSONResponse CreateActivityResponse + +func (response CreateActivity200JSONResponse) VisitCreateActivityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type CreateActivity400JSONResponse ErrorResponse + +func (response CreateActivity400JSONResponse) VisitCreateActivityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type CreateActivity401JSONResponse ErrorResponse + +func (response CreateActivity401JSONResponse) VisitCreateActivityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type CreateActivity500JSONResponse ErrorResponse + +func (response CreateActivity500JSONResponse) VisitCreateActivityResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type GetAdminRequestObject struct { } @@ -2730,6 +2866,50 @@ func (response GetProgressList500JSONResponse) VisitGetProgressListResponse(w ht return json.NewEncoder(w).Encode(response) } +type UpdateProgressRequestObject struct { + Body *UpdateProgressJSONRequestBody +} + +type UpdateProgressResponseObject interface { + VisitUpdateProgressResponse(w http.ResponseWriter) error +} + +type UpdateProgress200JSONResponse UpdateProgressResponse + +func (response UpdateProgress200JSONResponse) VisitUpdateProgressResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateProgress400JSONResponse ErrorResponse + +func (response UpdateProgress400JSONResponse) VisitUpdateProgressResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateProgress401JSONResponse ErrorResponse + +func (response UpdateProgress401JSONResponse) VisitUpdateProgressResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(401) + + return json.NewEncoder(w).Encode(response) +} + +type UpdateProgress500JSONResponse ErrorResponse + +func (response UpdateProgress500JSONResponse) VisitUpdateProgressResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + type GetProgressRequestObject struct { Id string `json:"id"` } @@ -2935,6 +3115,9 @@ type StrictServerInterface interface { // Get activity data // (GET /activity) GetActivity(ctx context.Context, request GetActivityRequestObject) (GetActivityResponseObject, error) + // Create activity records + // (POST /activity) + CreateActivity(ctx context.Context, request CreateActivityRequestObject) (CreateActivityResponseObject, error) // Get admin page data // (GET /admin) GetAdmin(ctx context.Context, request GetAdminRequestObject) (GetAdminResponseObject, error) @@ -3010,6 +3193,9 @@ type StrictServerInterface interface { // List progress records // (GET /progress) GetProgressList(ctx context.Context, request GetProgressListRequestObject) (GetProgressListResponseObject, error) + // Update document progress + // (PUT /progress) + UpdateProgress(ctx context.Context, request UpdateProgressRequestObject) (UpdateProgressResponseObject, error) // Get document progress // (GET /progress/{id}) GetProgress(ctx context.Context, request GetProgressRequestObject) (GetProgressResponseObject, error) @@ -3082,6 +3268,37 @@ func (sh *strictHandler) GetActivity(w http.ResponseWriter, r *http.Request, par } } +// CreateActivity operation middleware +func (sh *strictHandler) CreateActivity(w http.ResponseWriter, r *http.Request) { + var request CreateActivityRequestObject + + var body CreateActivityJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.CreateActivity(ctx, request.(CreateActivityRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CreateActivity") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(CreateActivityResponseObject); ok { + if err := validResponse.VisitCreateActivityResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // GetAdmin operation middleware func (sh *strictHandler) GetAdmin(w http.ResponseWriter, r *http.Request) { var request GetAdminRequestObject @@ -3764,6 +3981,37 @@ func (sh *strictHandler) GetProgressList(w http.ResponseWriter, r *http.Request, } } +// UpdateProgress operation middleware +func (sh *strictHandler) UpdateProgress(w http.ResponseWriter, r *http.Request) { + var request UpdateProgressRequestObject + + var body UpdateProgressJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.UpdateProgress(ctx, request.(UpdateProgressRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UpdateProgress") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(UpdateProgressResponseObject); ok { + if err := validResponse.VisitUpdateProgressResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // GetProgress operation middleware func (sh *strictHandler) GetProgress(w http.ResponseWriter, r *http.Request, id string) { var request GetProgressRequestObject diff --git a/api/v1/openapi.yaml b/api/v1/openapi.yaml index 94c7d28..c919463 100644 --- a/api/v1/openapi.yaml +++ b/api/v1/openapi.yaml @@ -92,9 +92,13 @@ components: type: string device_name: type: string + device_id: + type: string percentage: type: number format: double + progress: + type: string document_id: type: string user_id: @@ -103,6 +107,88 @@ components: type: string format: date-time + UpdateProgressRequest: + type: object + properties: + document_id: + type: string + percentage: + type: number + format: double + progress: + type: string + device_id: + type: string + device_name: + type: string + required: + - document_id + - percentage + - progress + - device_id + - device_name + + UpdateProgressResponse: + type: object + properties: + document_id: + type: string + timestamp: + type: string + format: date-time + required: + - document_id + - timestamp + + CreateActivityItem: + type: object + properties: + document_id: + type: string + start_time: + type: integer + format: int64 + duration: + type: integer + format: int64 + page: + type: integer + format: int64 + pages: + type: integer + format: int64 + required: + - document_id + - start_time + - duration + - page + - pages + + CreateActivityRequest: + type: object + properties: + device_id: + type: string + device_name: + type: string + activity: + type: array + items: + $ref: '#/components/schemas/CreateActivityItem' + required: + - device_id + - device_name + - activity + + CreateActivityResponse: + type: object + properties: + added: + type: integer + format: int64 + required: + - added + Activity: type: object properties: @@ -1003,6 +1089,44 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + put: + summary: Update document progress + operationId: updateProgress + tags: + - Progress + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProgressRequest' + responses: + 200: + description: Progress updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProgressResponse' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /progress/{id}: get: @@ -1093,6 +1217,44 @@ paths: application/json: schema: $ref: '#/components/schemas/ErrorResponse' + post: + summary: Create activity records + operationId: createActivity + tags: + - Activity + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateActivityRequest' + responses: + 200: + description: Activity created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/CreateActivityResponse' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 401: + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + 500: + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' /settings: get: diff --git a/api/v1/progress.go b/api/v1/progress.go index f5a694e..5461b28 100644 --- a/api/v1/progress.go +++ b/api/v1/progress.go @@ -3,9 +3,10 @@ package v1 import ( "context" "math" + "time" - "reichard.io/antholume/database" log "github.com/sirupsen/logrus" + "reichard.io/antholume/database" ) // GET /progress @@ -26,9 +27,9 @@ func (s *Server) GetProgressList(ctx context.Context, request GetProgressListReq } filter := database.GetProgressParams{ - UserID: auth.UserName, - Offset: (page - 1) * limit, - Limit: limit, + UserID: auth.UserName, + Offset: (page - 1) * limit, + Limit: limit, } if request.Params.Document != nil && *request.Params.Document != "" { @@ -45,7 +46,7 @@ func (s *Server) GetProgressList(ctx context.Context, request GetProgressListReq total := int64(len(progress)) var nextPage *int64 var previousPage *int64 - + // Calculate total pages totalPages := int64(math.Ceil(float64(total) / float64(limit))) if page < totalPages { @@ -58,13 +59,13 @@ func (s *Server) GetProgressList(ctx context.Context, request GetProgressListReq apiProgress := make([]Progress, len(progress)) for i, row := range progress { apiProgress[i] = Progress{ - Title: row.Title, - Author: row.Author, - DeviceName: &row.DeviceName, - Percentage: &row.Percentage, - DocumentId: &row.DocumentID, - UserId: &row.UserID, - CreatedAt: parseTimePtr(row.CreatedAt), + Title: row.Title, + Author: row.Author, + DeviceName: &row.DeviceName, + Percentage: &row.Percentage, + DocumentId: &row.DocumentID, + UserId: &row.UserID, + CreatedAt: parseTimePtr(row.CreatedAt), } } @@ -87,33 +88,23 @@ func (s *Server) GetProgress(ctx context.Context, request GetProgressRequestObje return GetProgress401JSONResponse{Code: 401, Message: "Unauthorized"}, nil } - filter := database.GetProgressParams{ - UserID: auth.UserName, - DocFilter: true, - DocumentID: request.Id, - Offset: 0, - Limit: 1, - } - - progress, err := s.db.Queries.GetProgress(ctx, filter) + row, err := s.db.Queries.GetDocumentProgress(ctx, database.GetDocumentProgressParams{ + UserID: auth.UserName, + DocumentID: request.Id, + }) if err != nil { - log.Error("GetProgress DB Error:", err) + log.Error("GetDocumentProgress DB Error:", err) return GetProgress404JSONResponse{Code: 404, Message: "Progress not found"}, nil } - if len(progress) == 0 { - return GetProgress404JSONResponse{Code: 404, Message: "Progress not found"}, nil - } - - row := progress[0] apiProgress := Progress{ - Title: row.Title, - Author: row.Author, - DeviceName: &row.DeviceName, - Percentage: &row.Percentage, - DocumentId: &row.DocumentID, - UserId: &row.UserID, - CreatedAt: parseTimePtr(row.CreatedAt), + DeviceName: &row.DeviceName, + DeviceId: &row.DeviceID, + Percentage: &row.Percentage, + Progress: &row.Progress, + DocumentId: &row.DocumentID, + UserId: &row.UserID, + CreatedAt: parseTimePtr(row.CreatedAt), } response := ProgressResponse{ @@ -121,4 +112,52 @@ func (s *Server) GetProgress(ctx context.Context, request GetProgressRequestObje } return GetProgress200JSONResponse(response), nil -} \ No newline at end of file +} + +// PUT /progress +func (s *Server) UpdateProgress(ctx context.Context, request UpdateProgressRequestObject) (UpdateProgressResponseObject, error) { + auth, ok := s.getSessionFromContext(ctx) + if !ok { + return UpdateProgress401JSONResponse{Code: 401, Message: "Unauthorized"}, nil + } + + if request.Body == nil { + return UpdateProgress400JSONResponse{Code: 400, Message: "Request body is required"}, nil + } + + if _, err := s.db.Queries.UpsertDevice(ctx, database.UpsertDeviceParams{ + ID: request.Body.DeviceId, + UserID: auth.UserName, + DeviceName: request.Body.DeviceName, + LastSynced: time.Now().UTC().Format(time.RFC3339), + }); err != nil { + log.Error("UpsertDevice DB Error:", err) + return UpdateProgress500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + if _, err := s.db.Queries.UpsertDocument(ctx, database.UpsertDocumentParams{ + ID: request.Body.DocumentId, + }); err != nil { + log.Error("UpsertDocument DB Error:", err) + return UpdateProgress500JSONResponse{Code: 500, Message: "Database error"}, nil + } + + progress, err := s.db.Queries.UpdateProgress(ctx, database.UpdateProgressParams{ + Percentage: request.Body.Percentage, + DocumentID: request.Body.DocumentId, + DeviceID: request.Body.DeviceId, + UserID: auth.UserName, + Progress: request.Body.Progress, + }) + if err != nil { + log.Error("UpdateProgress DB Error:", err) + return UpdateProgress400JSONResponse{Code: 400, Message: "Invalid request"}, nil + } + + response := UpdateProgressResponse{ + DocumentId: progress.DocumentID, + Timestamp: parseTime(progress.CreatedAt), + } + + return UpdateProgress200JSONResponse(response), nil +} diff --git a/frontend/src/generated/anthoLumeAPIV1.ts b/frontend/src/generated/anthoLumeAPIV1.ts index 4e9b3de..bfad9be 100644 --- a/frontend/src/generated/anthoLumeAPIV1.ts +++ b/frontend/src/generated/anthoLumeAPIV1.ts @@ -26,6 +26,8 @@ import type { import type { ActivityResponse, + CreateActivityRequest, + CreateActivityResponse, CreateDocumentBody, DirectoryListResponse, DocumentResponse, @@ -55,6 +57,8 @@ import type { SearchResponse, SettingsResponse, StreaksResponse, + UpdateProgressRequest, + UpdateProgressResponse, UpdateSettingsRequest, UpdateUserBody, UploadDocumentCoverBody, @@ -1079,6 +1083,112 @@ export function useGetProgressList { + + + + + return `/api/v1/progress` +} + +export const updateProgress = async (updateProgressRequest: UpdateProgressRequest, options?: RequestInit): Promise => { + + const res = await fetch(getUpdateProgressUrl(), + { + ...options, + method: 'PUT', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify( + updateProgressRequest,) + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: updateProgressResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as updateProgressResponse +} + + + + +export const getUpdateProgressMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: UpdateProgressRequest}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{data: UpdateProgressRequest}, TContext> => { + +const mutationKey = ['updateProgress']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {data: UpdateProgressRequest}> = (props) => { + const {data} = props ?? {}; + + return updateProgress(data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type UpdateProgressMutationResult = NonNullable>> + export type UpdateProgressMutationBody = UpdateProgressRequest + export type UpdateProgressMutationError = ErrorResponse + + /** + * @summary Update document progress + */ +export const useUpdateProgress = (options?: { mutation?:UseMutationOptions>, TError,{data: UpdateProgressRequest}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: UpdateProgressRequest}, + TContext + > => { + return useMutation(getUpdateProgressMutationOptions(options), queryClient); + } + /** * @summary Get document progress */ @@ -1349,6 +1459,112 @@ export function useGetActivity>, +/** + * @summary Create activity records + */ +export type createActivityResponse200 = { + data: CreateActivityResponse + status: 200 +} + +export type createActivityResponse400 = { + data: ErrorResponse + status: 400 +} + +export type createActivityResponse401 = { + data: ErrorResponse + status: 401 +} + +export type createActivityResponse500 = { + data: ErrorResponse + status: 500 +} + +export type createActivityResponseSuccess = (createActivityResponse200) & { + headers: Headers; +}; +export type createActivityResponseError = (createActivityResponse400 | createActivityResponse401 | createActivityResponse500) & { + headers: Headers; +}; + +export type createActivityResponse = (createActivityResponseSuccess | createActivityResponseError) + +export const getCreateActivityUrl = () => { + + + + + return `/api/v1/activity` +} + +export const createActivity = async (createActivityRequest: CreateActivityRequest, options?: RequestInit): Promise => { + + const res = await fetch(getCreateActivityUrl(), + { + ...options, + method: 'POST', + headers: { 'Content-Type': 'application/json', ...options?.headers }, + body: JSON.stringify( + createActivityRequest,) + } +) + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: createActivityResponse['data'] = body ? JSON.parse(body) : {} + return { data, status: res.status, headers: res.headers } as createActivityResponse +} + + + + +export const getCreateActivityMutationOptions = (options?: { mutation?:UseMutationOptions>, TError,{data: CreateActivityRequest}, TContext>, fetch?: RequestInit} +): UseMutationOptions>, TError,{data: CreateActivityRequest}, TContext> => { + +const mutationKey = ['createActivity']; +const {mutation: mutationOptions, fetch: fetchOptions} = options ? + options.mutation && 'mutationKey' in options.mutation && options.mutation.mutationKey ? + options + : {...options, mutation: {...options.mutation, mutationKey}} + : {mutation: { mutationKey, }, fetch: undefined}; + + + + + const mutationFn: MutationFunction>, {data: CreateActivityRequest}> = (props) => { + const {data} = props ?? {}; + + return createActivity(data,fetchOptions) + } + + + + + + + return { mutationFn, ...mutationOptions }} + + export type CreateActivityMutationResult = NonNullable>> + export type CreateActivityMutationBody = CreateActivityRequest + export type CreateActivityMutationError = ErrorResponse + + /** + * @summary Create activity records + */ +export const useCreateActivity = (options?: { mutation?:UseMutationOptions>, TError,{data: CreateActivityRequest}, TContext>, fetch?: RequestInit} + , queryClient?: QueryClient): UseMutationResult< + Awaited>, + TError, + {data: CreateActivityRequest}, + TContext + > => { + return useMutation(getCreateActivityMutationOptions(options), queryClient); + } + /** * @summary Get user settings */ diff --git a/frontend/src/generated/model/createActivityItem.ts b/frontend/src/generated/model/createActivityItem.ts new file mode 100644 index 0000000..cb6ad46 --- /dev/null +++ b/frontend/src/generated/model/createActivityItem.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface CreateActivityItem { + document_id: string; + start_time: number; + duration: number; + page: number; + pages: number; +} diff --git a/frontend/src/generated/model/createActivityRequest.ts b/frontend/src/generated/model/createActivityRequest.ts new file mode 100644 index 0000000..9b0ed02 --- /dev/null +++ b/frontend/src/generated/model/createActivityRequest.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ +import type { CreateActivityItem } from './createActivityItem'; + +export interface CreateActivityRequest { + device_id: string; + device_name: string; + activity: CreateActivityItem[]; +} diff --git a/frontend/src/generated/model/createActivityResponse.ts b/frontend/src/generated/model/createActivityResponse.ts new file mode 100644 index 0000000..d954dc1 --- /dev/null +++ b/frontend/src/generated/model/createActivityResponse.ts @@ -0,0 +1,11 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface CreateActivityResponse { + added: number; +} diff --git a/frontend/src/generated/model/index.ts b/frontend/src/generated/model/index.ts index 8b37270..9a4cba4 100644 --- a/frontend/src/generated/model/index.ts +++ b/frontend/src/generated/model/index.ts @@ -10,6 +10,9 @@ export * from './activity'; export * from './activityResponse'; export * from './backupType'; export * from './configResponse'; +export * from './createActivityItem'; +export * from './createActivityRequest'; +export * from './createActivityResponse'; export * from './createDocumentBody'; export * from './databaseInfo'; export * from './device'; @@ -57,6 +60,8 @@ export * from './setting'; export * from './settingsResponse'; export * from './streaksResponse'; export * from './updateDocumentBody'; +export * from './updateProgressRequest'; +export * from './updateProgressResponse'; export * from './updateSettingsRequest'; export * from './updateUserBody'; export * from './uploadDocumentCoverBody'; diff --git a/frontend/src/generated/model/progress.ts b/frontend/src/generated/model/progress.ts index a45db00..3a8403e 100644 --- a/frontend/src/generated/model/progress.ts +++ b/frontend/src/generated/model/progress.ts @@ -10,7 +10,9 @@ export interface Progress { title?: string; author?: string; device_name?: string; + device_id?: string; percentage?: number; + progress?: string; document_id?: string; user_id?: string; created_at?: string; diff --git a/frontend/src/generated/model/updateProgressRequest.ts b/frontend/src/generated/model/updateProgressRequest.ts new file mode 100644 index 0000000..0c7f8b3 --- /dev/null +++ b/frontend/src/generated/model/updateProgressRequest.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface UpdateProgressRequest { + document_id: string; + percentage: number; + progress: string; + device_id: string; + device_name: string; +} diff --git a/frontend/src/generated/model/updateProgressResponse.ts b/frontend/src/generated/model/updateProgressResponse.ts new file mode 100644 index 0000000..510827f --- /dev/null +++ b/frontend/src/generated/model/updateProgressResponse.ts @@ -0,0 +1,12 @@ +/** + * Generated by orval v8.5.3 🍺 + * Do not edit manually. + * AnthoLume API v1 + * REST API for AnthoLume document management system + * OpenAPI spec version: 1.0.0 + */ + +export interface UpdateProgressResponse { + document_id: string; + timestamp: string; +}