diff --git a/cmd/internal/openapi/client.gen.go b/cmd/internal/openapi/client.gen.go index 72cc9e6d..ec43735b 100644 --- a/cmd/internal/openapi/client.gen.go +++ b/cmd/internal/openapi/client.gen.go @@ -159,6 +159,48 @@ func (e DownloadTaskStatusState) Valid() bool { } } +// Defines values for DownloadTaskTimelineItemSeverity. +const ( + DownloadTaskTimelineItemSeverityError DownloadTaskTimelineItemSeverity = "error" + DownloadTaskTimelineItemSeverityInfo DownloadTaskTimelineItemSeverity = "info" + DownloadTaskTimelineItemSeveritySuccess DownloadTaskTimelineItemSeverity = "success" + DownloadTaskTimelineItemSeverityWarning DownloadTaskTimelineItemSeverity = "warning" +) + +// Valid indicates whether the value is a known member of the DownloadTaskTimelineItemSeverity enum. +func (e DownloadTaskTimelineItemSeverity) Valid() bool { + switch e { + case DownloadTaskTimelineItemSeverityError: + return true + case DownloadTaskTimelineItemSeverityInfo: + return true + case DownloadTaskTimelineItemSeveritySuccess: + return true + case DownloadTaskTimelineItemSeverityWarning: + return true + default: + return false + } +} + +// Defines values for DownloadTaskTimelineItemSource. +const ( + Activity DownloadTaskTimelineItemSource = "activity" + Task DownloadTaskTimelineItemSource = "task" +) + +// Valid indicates whether the value is a known member of the DownloadTaskTimelineItemSource enum. +func (e DownloadTaskTimelineItemSource) Valid() bool { + switch e { + case Activity: + return true + case Task: + return true + default: + return false + } +} + // Defines values for DownloaderEngine. const ( DownloaderEngineAria2 DownloaderEngine = "aria2" @@ -1136,13 +1178,13 @@ func (e UpdateStorageJSONBodyStatus) Valid() bool { // Defines values for CancelOrderJSONBodyStatus. const ( - CancelOrderJSONBodyStatusCanceled CancelOrderJSONBodyStatus = "canceled" + Canceled CancelOrderJSONBodyStatus = "canceled" ) // Valid indicates whether the value is a known member of the CancelOrderJSONBodyStatus enum. func (e CancelOrderJSONBodyStatus) Valid() bool { switch e { - case CancelOrderJSONBodyStatusCanceled: + case Canceled: return true default: return false @@ -1450,12 +1492,15 @@ type DownloadTask struct { ChargedCredits int64 `json:"chargedCredits"` State DownloadTaskStatusBillingState `json:"state"` } `json:"billing"` - Error *struct { + DownloadCompletedAt *string `json:"downloadCompletedAt"` + Error *struct { Code *string `json:"code,omitempty"` Message *string `json:"message"` } `json:"error"` - FinishedAt *string `json:"finishedAt"` - Output *struct { + FinishedAt *string `json:"finishedAt"` + IngestCompletedAt *string `json:"ingestCompletedAt"` + IngestStartedAt *string `json:"ingestStartedAt"` + Output *struct { ObjectId string `json:"objectId"` } `json:"output"` Progress struct { @@ -1470,7 +1515,9 @@ type DownloadTask struct { TotalBytes *int64 `json:"totalBytes,omitempty"` } `json:"upload"` } `json:"progress"` - Runtime *struct { + ResolveCompletedAt *string `json:"resolveCompletedAt"` + ResolveStartedAt *string `json:"resolveStartedAt"` + Runtime *struct { Connections *int `json:"connections,omitempty"` Engine *DownloadTaskStatusRuntimeEngine `json:"engine,omitempty"` EtaSeconds *int `json:"etaSeconds,omitempty"` @@ -1530,9 +1577,11 @@ type DownloadTask struct { } `json:"trackers,omitempty"` UpdatedAt *string `json:"updatedAt,omitempty"` } `json:"runtime"` - StartedAt *string `json:"startedAt"` - State DownloadTaskStatusState `json:"state"` - UpdatedAt string `json:"updatedAt"` + SeedingStartedAt *string `json:"seedingStartedAt"` + SeedingStoppedAt *string `json:"seedingStoppedAt"` + StartedAt *string `json:"startedAt"` + State DownloadTaskStatusState `json:"state"` + UpdatedAt string `json:"updatedAt"` } `json:"status"` } @@ -1559,6 +1608,30 @@ type DownloadTaskPage struct { Total int `json:"total"` } +// DownloadTaskTimeline defines model for DownloadTaskTimeline. +type DownloadTaskTimeline struct { + Items []DownloadTaskTimelineItem `json:"items"` +} + +// DownloadTaskTimelineItem defines model for DownloadTaskTimelineItem. +type DownloadTaskTimelineItem struct { + Action string `json:"action"` + Detail *string `json:"detail"` + Id string `json:"id"` + Metadata *map[string]*interface{} `json:"metadata"` + Severity DownloadTaskTimelineItemSeverity `json:"severity"` + Source DownloadTaskTimelineItemSource `json:"source"` + TaskId string `json:"taskId"` + Time string `json:"time"` + Title string `json:"title"` +} + +// DownloadTaskTimelineItemSeverity defines model for DownloadTaskTimelineItem.Severity. +type DownloadTaskTimelineItemSeverity string + +// DownloadTaskTimelineItemSource defines model for DownloadTaskTimelineItem.Source. +type DownloadTaskTimelineItemSource string + // Downloader defines model for Downloader. type Downloader struct { Arch string `json:"arch"` @@ -4650,6 +4723,9 @@ type ClientInterface interface { RetryDownloadTask(ctx context.Context, id string, body RetryDownloadTaskJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListDownloadTaskEvents request + ListDownloadTaskEvents(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) + // SetDownloadTaskStatusWithBody request with any body SetDownloadTaskStatusWithBody(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -7063,6 +7139,18 @@ func (c *Client) RetryDownloadTask(ctx context.Context, id string, body RetryDow return c.Client.Do(req) } +func (c *Client) ListDownloadTaskEvents(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListDownloadTaskEventsRequest(c.Server, id) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) SetDownloadTaskStatusWithBody(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewSetDownloadTaskStatusRequestWithBody(c.Server, id, contentType, body) if err != nil { @@ -13109,6 +13197,40 @@ func NewRetryDownloadTaskRequestWithBody(server string, id string, contentType s return req, nil } +// NewListDownloadTaskEventsRequest generates requests for ListDownloadTaskEvents +func NewListDownloadTaskEventsRequest(server string, id string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithOptions("simple", false, "id", id, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationPath, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/downloads/tasks/%s/events", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest(http.MethodGet, queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewSetDownloadTaskStatusRequest calls the generic SetDownloadTaskStatus builder with application/json body func NewSetDownloadTaskStatusRequest(server string, id string, body SetDownloadTaskStatusJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -18348,6 +18470,9 @@ type ClientWithResponsesInterface interface { RetryDownloadTaskWithResponse(ctx context.Context, id string, body RetryDownloadTaskJSONRequestBody, reqEditors ...RequestEditorFn) (*RetryDownloadTaskResponse, error) + // ListDownloadTaskEventsWithResponse request + ListDownloadTaskEventsWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*ListDownloadTaskEventsResponse, error) + // SetDownloadTaskStatusWithBodyWithResponse request with any body SetDownloadTaskStatusWithBodyWithResponse(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SetDownloadTaskStatusResponse, error) @@ -23849,6 +23974,40 @@ func (r RetryDownloadTaskResponse) ContentType() string { return "" } +type ListDownloadTaskEventsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *DownloadTaskTimeline + JSON401 *Error + JSON403 *Error + JSON404 *Error + JSON409 *Error +} + +// Status returns HTTPResponse.Status +func (r ListDownloadTaskEventsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListDownloadTaskEventsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// ContentType is a convenience method to retrieve the Content-Type value from the HTTP response headers +func (r ListDownloadTaskEventsResponse) ContentType() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Header.Get("Content-Type") + } + return "" +} + type SetDownloadTaskStatusResponse struct { Body []byte HTTPResponse *http.Response @@ -28893,6 +29052,15 @@ func (c *ClientWithResponses) RetryDownloadTaskWithResponse(ctx context.Context, return ParseRetryDownloadTaskResponse(rsp) } +// ListDownloadTaskEventsWithResponse request returning *ListDownloadTaskEventsResponse +func (c *ClientWithResponses) ListDownloadTaskEventsWithResponse(ctx context.Context, id string, reqEditors ...RequestEditorFn) (*ListDownloadTaskEventsResponse, error) { + rsp, err := c.ListDownloadTaskEvents(ctx, id, reqEditors...) + if err != nil { + return nil, err + } + return ParseListDownloadTaskEventsResponse(rsp) +} + // SetDownloadTaskStatusWithBodyWithResponse request with arbitrary body returning *SetDownloadTaskStatusResponse func (c *ClientWithResponses) SetDownloadTaskStatusWithBodyWithResponse(ctx context.Context, id string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SetDownloadTaskStatusResponse, error) { rsp, err := c.SetDownloadTaskStatusWithBody(ctx, id, contentType, body, reqEditors...) @@ -38025,6 +38193,60 @@ func ParseRetryDownloadTaskResponse(rsp *http.Response) (*RetryDownloadTaskRespo return response, nil } +// ParseListDownloadTaskEventsResponse parses an HTTP response from a ListDownloadTaskEventsWithResponse call +func ParseListDownloadTaskEventsResponse(rsp *http.Response) (*ListDownloadTaskEventsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ListDownloadTaskEventsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest DownloadTaskTimeline + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON409 = &dest + + } + + return response, nil +} + // ParseSetDownloadTaskStatusResponse parses an HTTP response from a SetDownloadTaskStatusWithResponse call func ParseSetDownloadTaskStatusResponse(rsp *http.Response) (*SetDownloadTaskStatusResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/migrations/0053_striped_pride.sql b/migrations/0053_striped_pride.sql new file mode 100644 index 00000000..758eb1e2 --- /dev/null +++ b/migrations/0053_striped_pride.sql @@ -0,0 +1,7 @@ +ALTER TABLE `download_tasks` ADD `resolve_started_at` integer;--> statement-breakpoint +ALTER TABLE `download_tasks` ADD `resolve_completed_at` integer;--> statement-breakpoint +ALTER TABLE `download_tasks` ADD `download_completed_at` integer;--> statement-breakpoint +ALTER TABLE `download_tasks` ADD `ingest_started_at` integer;--> statement-breakpoint +ALTER TABLE `download_tasks` ADD `ingest_completed_at` integer;--> statement-breakpoint +ALTER TABLE `download_tasks` ADD `seeding_started_at` integer;--> statement-breakpoint +ALTER TABLE `download_tasks` ADD `seeding_stopped_at` integer; \ No newline at end of file diff --git a/migrations/meta/0053_snapshot.json b/migrations/meta/0053_snapshot.json new file mode 100644 index 00000000..8dc394d8 --- /dev/null +++ b/migrations/meta/0053_snapshot.json @@ -0,0 +1,3967 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "788eaeff-a3a7-4da9-aadb-ab59817b494f", + "prevId": "fcd723fa-bb9f-4049-b9ed-6ac894da0b5e", + "tables": { + "activity_events": { + "name": "activity_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_name": { + "name": "target_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "announcements": { + "name": "announcements", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "published_at": { + "name": "published_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "announcements_status_priority_idx": { + "name": "announcements_status_priority_idx", + "columns": [ + "status", + "priority" + ], + "isUnique": false + }, + "announcements_published_idx": { + "name": "announcements_published_idx", + "columns": [ + "published_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "background_jobs": { + "name": "background_jobs", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "target_folder": { + "name": "target_folder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_path": { + "name": "target_path", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "input_bytes": { + "name": "input_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_bytes": { + "name": "output_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "processed_bytes": { + "name": "processed_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "file_count": { + "name": "file_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "current_filename": { + "name": "current_filename", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_metadata": { + "name": "result_metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "retryable": { + "name": "retryable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "cancelable": { + "name": "cancelable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "retried_from_job_id": { + "name": "retried_from_job_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "background_jobs_org_created_idx": { + "name": "background_jobs_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "background_jobs_org_status_idx": { + "name": "background_jobs_org_status_idx", + "columns": [ + "org_id", + "status" + ], + "isUnique": false + }, + "background_jobs_org_type_idx": { + "name": "background_jobs_org_type_idx", + "columns": [ + "org_id", + "type" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "cloud_traffic_reports": { + "name": "cloud_traffic_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "unit_bytes": { + "name": "unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "credits_per_unit": { + "name": "credits_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "cloud_traffic_reports_event_uniq": { + "name": "cloud_traffic_reports_event_uniq", + "columns": [ + "event_id" + ], + "isUnique": true + }, + "cloud_traffic_reports_org_period_idx": { + "name": "cloud_traffic_reports_org_period_idx", + "columns": [ + "org_id", + "period" + ], + "isUnique": false + }, + "cloud_traffic_reports_status_idx": { + "name": "cloud_traffic_reports_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "download_tasks": { + "name": "download_tasks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_uri": { + "name": "source_uri", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "target_folder": { + "name": "target_folder", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "tags": { + "name": "tags", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "assigned_downloader_id": { + "name": "assigned_downloader_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "billing_authorized_bytes": { + "name": "billing_authorized_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billing_charged_bytes": { + "name": "billing_charged_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billing_charged_credits": { + "name": "billing_charged_credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "billing_status": { + "name": "billing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'none'" + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "result_object_id": { + "name": "result_object_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "runtime": { + "name": "runtime", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolve_started_at": { + "name": "resolve_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "resolve_completed_at": { + "name": "resolve_completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "download_completed_at": { + "name": "download_completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ingest_started_at": { + "name": "ingest_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ingest_completed_at": { + "name": "ingest_completed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seeding_started_at": { + "name": "seeding_started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "seeding_stopped_at": { + "name": "seeding_stopped_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "download_tasks_org_created_idx": { + "name": "download_tasks_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "download_tasks_org_status_idx": { + "name": "download_tasks_org_status_idx", + "columns": [ + "org_id", + "status" + ], + "isUnique": false + }, + "download_tasks_org_category_idx": { + "name": "download_tasks_org_category_idx", + "columns": [ + "org_id", + "category" + ], + "isUnique": false + }, + "download_tasks_org_tags_idx": { + "name": "download_tasks_org_tags_idx", + "columns": [ + "org_id", + "tags" + ], + "isUnique": false + }, + "download_tasks_downloader_idx": { + "name": "download_tasks_downloader_idx", + "columns": [ + "assigned_downloader_id", + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "downloaders": { + "name": "downloaders", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token_jti": { + "name": "token_jti", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'offline'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "hostname": { + "name": "hostname", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "platform": { + "name": "platform", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "arch": { + "name": "arch", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "engine": { + "name": "engine", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'http'" + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "max_concurrent_tasks": { + "name": "max_concurrent_tasks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "current_tasks": { + "name": "current_tasks", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "download_bps": { + "name": "download_bps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "upload_bps": { + "name": "upload_bps", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "free_disk_bytes": { + "name": "free_disk_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remote_download_credit_billing_enabled": { + "name": "remote_download_credit_billing_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "remote_download_credit_unit_bytes": { + "name": "remote_download_credit_unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 104857600 + }, + "remote_download_credit_per_unit": { + "name": "remote_download_credit_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "downloaders_token_jti_unique": { + "name": "downloaders_token_jti_unique", + "columns": [ + "token_jti" + ], + "isUnique": true + }, + "downloaders_status_idx": { + "name": "downloaders_status_idx", + "columns": [ + "status" + ], + "isUnique": false + }, + "downloaders_enabled_idx": { + "name": "downloaders_enabled_idx", + "columns": [ + "enabled" + ], + "isUnique": false + }, + "downloaders_created_idx": { + "name": "downloaders_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "image_hosting_configs": { + "name": "image_hosting_configs", + "columns": { + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "custom_domain": { + "name": "custom_domain", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cf_hostname_id": { + "name": "cf_hostname_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "domain_verified_at": { + "name": "domain_verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "referer_allowlist": { + "name": "referer_allowlist", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "image_hosting_configs_custom_domain_unique": { + "name": "image_hosting_configs_custom_domain_unique", + "columns": [ + "custom_domain" + ], + "isUnique": true + } + }, + "foreignKeys": { + "image_hosting_configs_org_id_organization_id_fk": { + "name": "image_hosting_configs_org_id_organization_id_fk", + "tableFrom": "image_hosting_configs", + "tableTo": "organization", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "image_hostings": { + "name": "image_hostings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime": { + "name": "mime", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "access_count": { + "name": "access_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "last_accessed_at": { + "name": "last_accessed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "image_hostings_token_unique": { + "name": "image_hostings_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "image_hostings_org_path_uniq": { + "name": "image_hostings_org_path_uniq", + "columns": [ + "org_id", + "path" + ], + "isUnique": true + }, + "image_hostings_org_created_idx": { + "name": "image_hostings_org_created_idx", + "columns": [ + "org_id", + "created_at" + ], + "isUnique": false + }, + "image_hostings_token_idx": { + "name": "image_hostings_token_idx", + "columns": [ + "token" + ], + "isUnique": false + } + }, + "foreignKeys": { + "image_hostings_org_id_organization_id_fk": { + "name": "image_hostings_org_id_organization_id_fk", + "tableFrom": "image_hostings", + "tableTo": "organization", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "image_hostings_storage_id_storages_id_fk": { + "name": "image_hostings_storage_id_storages_id_fk", + "tableFrom": "image_hostings", + "tableTo": "storages", + "columnsFrom": [ + "storage_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invite_codes": { + "name": "invite_codes", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "used_by": { + "name": "used_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "used_at": { + "name": "used_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_codes_code_unique": { + "name": "invite_codes_code_unique", + "columns": [ + "code" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "license_bindings": { + "name": "license_bindings", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "cloud_binding_id": { + "name": "cloud_binding_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_store_id": { + "name": "cloud_store_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_account_id": { + "name": "cloud_account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cloud_account_email": { + "name": "cloud_account_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cached_certificate": { + "name": "cached_certificate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cached_certificate_expires_at": { + "name": "cached_certificate_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bound_at": { + "name": "bound_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "disconnected_at": { + "name": "disconnected_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refresh_at": { + "name": "last_refresh_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refresh_error": { + "name": "last_refresh_error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "license_bindings_active_uniq": { + "name": "license_bindings_active_uniq", + "columns": [ + "status" + ], + "isUnique": true, + "where": "status = 'active'" + }, + "license_bindings_cloud_binding_idx": { + "name": "license_bindings_cloud_binding_idx", + "columns": [ + "cloud_binding_id" + ], + "isUnique": false + }, + "license_bindings_instance_idx": { + "name": "license_bindings_instance_idx", + "columns": [ + "instance_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "matters": { + "name": "matters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "alias": { + "name": "alias", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "dirtype": { + "name": "dirtype", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "parent": { + "name": "parent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "object": { + "name": "object", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'draft'" + }, + "trashed_at": { + "name": "trashed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "matters_alias_unique": { + "name": "matters_alias_unique", + "columns": [ + "alias" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "notifications": { + "name": "notifications", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "ref_type": { + "name": "ref_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ref_id": { + "name": "ref_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "read_at": { + "name": "read_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "notifications_user_created_idx": { + "name": "notifications_user_created_idx", + "columns": [ + "user_id", + "created_at" + ], + "isUnique": false + }, + "notifications_user_read_idx": { + "name": "notifications_user_read_idx", + "columns": [ + "user_id", + "read_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "object_upload_sessions": { + "name": "object_upload_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "object_id": { + "name": "object_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_id": { + "name": "storage_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upload_id": { + "name": "upload_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "part_size": { + "name": "part_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "on_conflict": { + "name": "on_conflict", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fail'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "object_upload_sessions_object_idx": { + "name": "object_upload_sessions_object_idx", + "columns": [ + "org_id", + "object_id" + ], + "isUnique": false + }, + "object_upload_sessions_expires_idx": { + "name": "object_upload_sessions_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "org_quota_entitlements": { + "name": "org_quota_entitlements", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_type": { + "name": "resource_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "entitlement_type": { + "name": "entitlement_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'grant'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bytes": { + "name": "bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "starts_at": { + "name": "starts_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "org_quota_entitlements_org_resource_idx": { + "name": "org_quota_entitlements_org_resource_idx", + "columns": [ + "org_id", + "resource_type", + "status" + ], + "isUnique": false + }, + "org_quota_entitlements_org_type_idx": { + "name": "org_quota_entitlements_org_type_idx", + "columns": [ + "org_id", + "resource_type", + "entitlement_type", + "status" + ], + "isUnique": false + }, + "org_quota_entitlements_active_plan_uniq": { + "name": "org_quota_entitlements_active_plan_uniq", + "columns": [ + "org_id", + "resource_type", + "entitlement_type" + ], + "isUnique": true, + "where": "status = 'active' AND entitlement_type = 'plan'" + }, + "org_quota_entitlements_source_resource_uniq": { + "name": "org_quota_entitlements_source_resource_uniq", + "columns": [ + "source", + "source_id", + "resource_type" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "org_quotas": { + "name": "org_quotas", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "quota": { + "name": "quota", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "used": { + "name": "used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_quota": { + "name": "traffic_quota", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_used": { + "name": "traffic_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "traffic_period": { + "name": "traffic_period", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'1970-01'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "remote_download_usage_reports": { + "name": "remote_download_usage_reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "downloader_id": { + "name": "downloader_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "task_id": { + "name": "task_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unit_index": { + "name": "unit_index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "unit_bytes": { + "name": "unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credits_per_unit": { + "name": "credits_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "remote_download_usage_reports_event_id_unique": { + "name": "remote_download_usage_reports_event_id_unique", + "columns": [ + "event_id" + ], + "isUnique": true + }, + "remote_download_usage_task_unit_uniq": { + "name": "remote_download_usage_task_unit_uniq", + "columns": [ + "task_id", + "unit_index" + ], + "isUnique": true + }, + "remote_download_usage_org_idx": { + "name": "remote_download_usage_org_idx", + "columns": [ + "org_id" + ], + "isUnique": false + }, + "remote_download_usage_status_idx": { + "name": "remote_download_usage_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "share_recipients": { + "name": "share_recipients", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "share_id": { + "name": "share_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "recipient_user_id": { + "name": "recipient_user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipient_email": { + "name": "recipient_email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "share_recipients_share_id_idx": { + "name": "share_recipients_share_id_idx", + "columns": [ + "share_id" + ], + "isUnique": false + }, + "share_recipients_user_id_idx": { + "name": "share_recipients_user_id_idx", + "columns": [ + "recipient_user_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "shares": { + "name": "shares", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "matter_id": { + "name": "matter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "download_limit": { + "name": "download_limit", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "downloads": { + "name": "downloads", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "shares_token_unique": { + "name": "shares_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "shares_creator_status_created_idx": { + "name": "shares_creator_status_created_idx", + "columns": [ + "creator_id", + "status", + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "site_invitations": { + "name": "site_invitations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accepted_by": { + "name": "accepted_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_by": { + "name": "revoked_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "site_invitations_token_unique": { + "name": "site_invitations_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "site_invitations_email_idx": { + "name": "site_invitations_email_idx", + "columns": [ + "email" + ], + "isUnique": false + }, + "site_invitations_created_idx": { + "name": "site_invitations_created_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "site_invitations_expires_idx": { + "name": "site_invitations_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "storages": { + "name": "storages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "bucket": { + "name": "bucket", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'auto'" + }, + "access_key": { + "name": "access_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secret_key": { + "name": "secret_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_path": { + "name": "file_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "custom_host": { + "name": "custom_host", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "''" + }, + "capacity": { + "name": "capacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "egress_credit_billing_enabled": { + "name": "egress_credit_billing_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "egress_credit_unit_bytes": { + "name": "egress_credit_unit_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 104857600 + }, + "egress_credit_per_unit": { + "name": "egress_credit_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "force_path_style": { + "name": "force_path_style", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "used": { + "name": "used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "system_options": { + "name": "system_options", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "public": { + "name": "public", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "team_invite_links": { + "name": "team_invite_links", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "team_invite_links_token_unique": { + "name": "team_invite_links_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webdav_dead_properties": { + "name": "webdav_dead_properties", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_path": { + "name": "resource_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "webdav_dead_properties_resource_prop_uniq": { + "name": "webdav_dead_properties_resource_prop_uniq", + "columns": [ + "org_id", + "resource_path", + "namespace", + "name" + ], + "isUnique": true + }, + "webdav_dead_properties_resource_idx": { + "name": "webdav_dead_properties_resource_idx", + "columns": [ + "org_id", + "resource_path" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webdav_locks": { + "name": "webdav_locks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "org_id": { + "name": "org_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "resource_path": { + "name": "resource_path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "depth": { + "name": "depth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'infinity'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "webdav_locks_token_unique": { + "name": "webdav_locks_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "webdav_locks_resource_idx": { + "name": "webdav_locks_resource_idx", + "columns": [ + "org_id", + "resource_path" + ], + "isUnique": false + }, + "webdav_locks_expires_idx": { + "name": "webdav_locks_expires_idx", + "columns": [ + "expires_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "webhook_events": { + "name": "webhook_events", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cloud'" + }, + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'order.quota_changed'" + }, + "payload_hash": { + "name": "payload_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "raw_payload": { + "name": "raw_payload", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "processed_at": { + "name": "processed_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "webhook_events_source_event_uniq": { + "name": "webhook_events_source_event_uniq", + "columns": [ + "source", + "event_id" + ], + "isUnique": true + }, + "webhook_events_source_created_idx": { + "name": "webhook_events_source_created_idx", + "columns": [ + "source", + "created_at" + ], + "isUnique": false + }, + "webhook_events_status_idx": { + "name": "webhook_events_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "apikey": { + "name": "apikey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_request": { + "name": "last_request", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "apikey_config_id_idx": { + "name": "apikey_config_id_idx", + "columns": [ + "config_id" + ], + "isUnique": false + }, + "apikey_reference_id_idx": { + "name": "apikey_reference_id_idx", + "columns": [ + "reference_id" + ], + "isUnique": false + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + "key" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "deviceCode": { + "name": "deviceCode", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "device_code": { + "name": "device_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_code": { + "name": "user_code", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "last_polled_at": { + "name": "last_polled_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "polling_interval": { + "name": "polling_interval", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "deviceCode_device_code_idx": { + "name": "deviceCode_device_code_idx", + "columns": [ + "device_code" + ], + "isUnique": false + }, + "deviceCode_user_code_idx": { + "name": "deviceCode_user_code_idx", + "columns": [ + "user_code" + ], + "isUnique": false + }, + "deviceCode_status_idx": { + "name": "deviceCode_status_idx", + "columns": [ + "status" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "invitation": { + "name": "invitation", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invitation_organizationId_idx": { + "name": "invitation_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + "email" + ], + "isUnique": false + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": [ + "inviter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "member": { + "name": "member", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'member'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "member_organizationId_idx": { + "name": "member_organizationId_idx", + "columns": [ + "organization_id" + ], + "isUnique": false + }, + "member_userId_idx": { + "name": "member_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": [ + "organization_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "organization": { + "name": "organization", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": [ + "slug" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "banned": { + "name": "banned", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_username": { + "name": "display_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "user_username_unique": { + "name": "user_username_unique", + "columns": [ + "username" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index b5813f49..223e757b 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -365,6 +365,13 @@ "when": 1782761700959, "tag": "0052_rename-downloader-http-engine", "breakpoints": true + }, + { + "idx": 53, + "version": "6", + "when": 1782781065387, + "tag": "0053_striped_pride", + "breakpoints": true } ] } \ No newline at end of file diff --git a/server/adapters/repos/activity.ts b/server/adapters/repos/activity.ts index 1124ff6d..44c9080c 100644 --- a/server/adapters/repos/activity.ts +++ b/server/adapters/repos/activity.ts @@ -122,5 +122,34 @@ export function createActivityRepo(db: Database): ActivityRepo { return { items, total, page, pageSize } }, + + async listByTarget(opts) { + const page = opts.page ?? 1 + const pageSize = Math.min(100, Math.max(1, opts.pageSize ?? 50)) + const offset = (page - 1) * pageSize + const whereClause = and( + eq(activityEvents.orgId, opts.orgId), + eq(activityEvents.targetType, opts.targetType), + eq(activityEvents.targetId, opts.targetId), + ) + + const [countRows, rows] = await Promise.all([ + db.select({ count: count() }).from(activityEvents).where(whereClause), + db + .select() + .from(activityEvents) + .where(whereClause) + .orderBy(desc(activityEvents.createdAt)) + .limit(pageSize) + .offset(offset), + ]) + + return { + items: rows, + total: countRows[0]?.count ?? 0, + page, + pageSize, + } + }, } } diff --git a/server/adapters/repos/download-task.ts b/server/adapters/repos/download-task.ts index b0852ad9..0fcbd23d 100644 --- a/server/adapters/repos/download-task.ts +++ b/server/adapters/repos/download-task.ts @@ -57,6 +57,13 @@ function toRecord(row: DownloadTaskRow): DownloadTaskRecord { errorMessage: row.errorMessage, resultObjectId: row.resultObjectId, runtime: row.runtime, + resolveStartedAt: row.resolveStartedAt, + resolveCompletedAt: row.resolveCompletedAt, + downloadCompletedAt: row.downloadCompletedAt, + ingestStartedAt: row.ingestStartedAt, + ingestCompletedAt: row.ingestCompletedAt, + seedingStartedAt: row.seedingStartedAt, + seedingStoppedAt: row.seedingStoppedAt, createdAt: row.createdAt, updatedAt: row.updatedAt, assignedAt: row.assignedAt, @@ -101,6 +108,13 @@ function toDownloadTask(row: DownloadTaskRow): DownloadTask { output: row.resultObjectId ? { objectId: row.resultObjectId } : null, runtime, error: row.errorMessage ? { code: row.errorCode, message: row.errorMessage } : null, + resolveStartedAt: row.resolveStartedAt?.toISOString() ?? null, + resolveCompletedAt: row.resolveCompletedAt?.toISOString() ?? null, + downloadCompletedAt: row.downloadCompletedAt?.toISOString() ?? null, + ingestStartedAt: row.ingestStartedAt?.toISOString() ?? null, + ingestCompletedAt: row.ingestCompletedAt?.toISOString() ?? null, + seedingStartedAt: row.seedingStartedAt?.toISOString() ?? null, + seedingStoppedAt: row.seedingStoppedAt?.toISOString() ?? null, startedAt: row.startedAt?.toISOString() ?? null, finishedAt: row.finishedAt?.toISOString() ?? null, updatedAt: row.updatedAt.toISOString(), diff --git a/server/db/schema.ts b/server/db/schema.ts index b478f8b8..3ebe0fc5 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -349,6 +349,13 @@ export const downloadTasks = sqliteTable( errorMessage: text('error_message'), resultObjectId: text('result_object_id'), runtime: text('runtime'), + resolveStartedAt: integer('resolve_started_at', { mode: 'timestamp_ms' }), + resolveCompletedAt: integer('resolve_completed_at', { mode: 'timestamp_ms' }), + downloadCompletedAt: integer('download_completed_at', { mode: 'timestamp_ms' }), + ingestStartedAt: integer('ingest_started_at', { mode: 'timestamp_ms' }), + ingestCompletedAt: integer('ingest_completed_at', { mode: 'timestamp_ms' }), + seedingStartedAt: integer('seeding_started_at', { mode: 'timestamp_ms' }), + seedingStoppedAt: integer('seeding_stopped_at', { mode: 'timestamp_ms' }), createdAt: integer('created_at', { mode: 'timestamp_ms' }).notNull(), updatedAt: integer('updated_at', { mode: 'timestamp_ms' }).notNull(), assignedAt: integer('assigned_at', { mode: 'timestamp_ms' }), diff --git a/server/http/downloads/download-tasks.integration.test.ts b/server/http/downloads/download-tasks.integration.test.ts index d4c95dc4..5ad40084 100644 --- a/server/http/downloads/download-tasks.integration.test.ts +++ b/server/http/downloads/download-tasks.integration.test.ts @@ -1,4 +1,4 @@ -import type { Downloader, DownloadTask } from '@shared/types' +import type { Downloader, DownloadTask, DownloadTaskTimelineItem } from '@shared/types' import { sql } from 'drizzle-orm' import { beforeEach, describe, expect, it, vi } from 'vitest' import { S3Service } from '../../adapters/gateways/s3.js' @@ -1196,6 +1196,95 @@ describe('Download tasks API integration', () => { expect(seedingTask.status.runtime).not.toHaveProperty('etaSeconds') }) + it('returns a task timeline from lifecycle fields and activity events [spec: download-tasks/events]', async () => { + const { app, db } = await createTestApp({ DOWNLOAD_TOKEN_SECRET: 'test-download-token-secret' }) + await insertStorage(db) + + const createdDownloader = await registerDownloaderThroughDeviceLogin(app, 'timeline-downloader') + const downloaderHeaders = { + Authorization: `Bearer ${createdDownloader.token}`, + 'Content-Type': 'application/json', + } + await recordDownloaderHeartbeat(app, createdDownloader.token, { ...heartbeat, currentTasks: 0 }) + + const user = await authedHeaders(app, 'timeline-user@example.com') + const createTaskRes = await app.request('/api/downloads/tasks', { + method: 'POST', + headers: { ...user, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + source: { type: 'magnet', uri: 'magnet:?xt=urn:btih:timeline' }, + targetFolder: 'Remote Downloads', + name: 'timeline.torrent', + }), + }) + expect(createTaskRes.status).toBe(201) + const task = (await createTaskRes.json()) as DownloadTask + await claimTaskForDownloader(app, createdDownloader.token, task.id) + + const resolvingRes = await app.request(`/api/downloads/tasks/${task.id}`, { + method: 'PATCH', + headers: downloaderHeaders, + body: JSON.stringify({ + status: 'downloading', + runtime: { + engine: 'aria2', + phase: 'metadata', + torrent: { infoHash: 'timeline-info-hash', peers: 0, seeders: 0 }, + trackers: [{ url: 'udp://tracker.example/announce', status: 'announce' }], + }, + }), + }) + expect(resolvingRes.status).toBe(200) + const resolvingTask = (await resolvingRes.json()) as DownloadTask + expect(resolvingTask.status.resolveStartedAt).toBeTruthy() + + const totalBytes = 1024 * 1024 + const ingestingRes = await app.request(`/api/downloads/tasks/${task.id}`, { + method: 'PATCH', + headers: downloaderHeaders, + body: JSON.stringify({ + status: 'uploading', + ...transferProgress({ downloadBytes: totalBytes, uploadBytes: 512, totalBytes }), + runtime: { + engine: 'aria2', + phase: 'uploading', + progress: { + download: { bytes: totalBytes, totalBytes, bytesPerSecond: 0 }, + upload: { bytes: 512, totalBytes, bytesPerSecond: 128 }, + }, + torrent: { infoHash: 'timeline-info-hash', peers: 2, seeders: 1 }, + }, + }), + }) + expect(ingestingRes.status).toBe(200) + const ingestingTask = (await ingestingRes.json()) as DownloadTask + expect(ingestingTask.status.resolveCompletedAt).toBeTruthy() + expect(ingestingTask.status.downloadCompletedAt).toBeTruthy() + expect(ingestingTask.status.ingestStartedAt).toBeTruthy() + + const eventsRes = await app.request(`/api/downloads/tasks/${task.id}/events`, { headers: user }) + expect(eventsRes.status).toBe(200) + const body = (await eventsRes.json()) as { items: DownloadTaskTimelineItem[] } + const actions = body.items.map((item) => item.action) + expect(actions).toEqual( + expect.arrayContaining([ + 'download_task_created', + 'download_task_assigned', + 'download_resolve_started', + 'download_resolve_completed', + 'download_completed', + 'download_ingest_started', + 'download_task_ingesting', + ]), + ) + expect(body.items[0]?.time).toBeTruthy() + expect(body.items.find((item) => item.action === 'download_resolve_started')?.metadata).toMatchObject({ + engine: 'aria2', + phase: 'metadata', + trackerCount: 1, + }) + }) + it('returns storage failure details when multipart upload session creation fails [spec: download-tasks/upload-session-failure]', async () => { vi.mocked(S3Service.prototype.createMultipartUpload).mockRejectedValueOnce( new Error('bucket does not support multipart'), diff --git a/server/http/downloads/download-tasks.ts b/server/http/downloads/download-tasks.ts index 7df1d022..650d449d 100644 --- a/server/http/downloads/download-tasks.ts +++ b/server/http/downloads/download-tasks.ts @@ -5,6 +5,7 @@ import { downloadTaskPageSchema, downloadTaskSchema, downloadTaskStatusUpdateSchema, + downloadTaskTimelineSchema, listDownloadTasksQuerySchema, updateDownloadTaskSchema, } from '@shared/schemas' @@ -13,6 +14,7 @@ import type { Env } from '../../middleware/platform' import { createDownloadTask, getDownloadTask, + getDownloadTaskTimeline, listDownloadTasks, performDownloadTaskAction, updateDownloadTask, @@ -101,6 +103,20 @@ const getRoute = createRoute({ }, }) +const eventsRoute = createRoute({ + operationId: 'listDownloadTaskEvents', + summary: 'List download task timeline events', + tags: ['Download Tasks'], + method: 'get', + path: '/{id}/events', + middleware: [requirePermission('remoteDownload', 'read')] as const, + request: { params: z.object({ id: z.string() }) }, + responses: { + 200: jsonContent(downloadTaskTimelineSchema, 'Download task timeline'), + ...taskErrorResponses, + }, +}) + const updateRoute = createRoute({ operationId: 'updateDownloadTask', summary: 'Update download task', @@ -206,6 +222,11 @@ const downloadTasksRoute = new OpenAPIHono() if (!orgId) throw unauthorized() return c.json(await getDownloadTask(c.get('deps'), orgId, c.req.valid('param').id), 200) }) + .openapi(eventsRoute, async (c) => { + const orgId = c.get('orgId') + if (!orgId) throw unauthorized() + return c.json(await getDownloadTaskTimeline(c.get('deps'), orgId, c.req.valid('param').id), 200) + }) .openapi(statusRoute, async (c) => { const orgId = c.get('orgId') if (!orgId) throw unauthorized() diff --git a/server/test/setup.ts b/server/test/setup.ts index cb5cc2a8..ed4c6c4d 100644 --- a/server/test/setup.ts +++ b/server/test/setup.ts @@ -441,6 +441,13 @@ const APP_SCHEMA_SQL = ` error_message TEXT, result_object_id TEXT, runtime TEXT, + resolve_started_at INTEGER, + resolve_completed_at INTEGER, + download_completed_at INTEGER, + ingest_started_at INTEGER, + ingest_completed_at INTEGER, + seeding_started_at INTEGER, + seeding_stopped_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, assigned_at INTEGER, diff --git a/server/usecases/downloads/downloads.test.ts b/server/usecases/downloads/downloads.test.ts index 6a2f76fd..b447b9e6 100644 --- a/server/usecases/downloads/downloads.test.ts +++ b/server/usecases/downloads/downloads.test.ts @@ -69,6 +69,7 @@ function makeDeps(downloaders: Partial = {}) { licenseBinding: {}, licensingCloud: {}, remoteDownloadUsage: {}, + activity: {}, } as DownloadsDeps, update, } diff --git a/server/usecases/downloads/downloads.ts b/server/usecases/downloads/downloads.ts index 1ff97a39..777d4d8a 100644 --- a/server/usecases/downloads/downloads.ts +++ b/server/usecases/downloads/downloads.ts @@ -9,12 +9,14 @@ import type { UpdateDownloadTaskInput, } from '@shared/schemas' import { downloadTaskRuntimeSchema } from '@shared/schemas' -import type { Downloader, DownloadTask, DownloadTaskRuntime } from '@shared/types' +import type { Downloader, DownloadTask, DownloadTaskRuntime, DownloadTaskTimelineItem } from '@shared/types' import { nanoid } from 'nanoid' import { ZPAN_CLOUD_URL_DEFAULT } from '../../../shared/constants' import { hasFeature } from '../../domain/licensing' import type { Platform } from '../../platform/interface' import type { + ActivityEvent, + ActivityRepo, DownloaderRepo, DownloadTaskRecord, DownloadTaskRepo, @@ -23,6 +25,7 @@ import type { LicensingCloudGateway, ListDownloadTasksFilters, RemoteDownloadUsageRepo, + UpdateDownloadTaskFields, } from '../ports' import { DownloadError, featureBlocked } from '../ports' import { loadBindingState } from '../site/licensing' @@ -41,6 +44,7 @@ export type DownloadsDeps = { licenseBinding: LicenseBindingRepo licensingCloud: LicensingCloudGateway remoteDownloadUsage: RemoteDownloadUsageRepo + activity: ActivityRepo } const DEFAULT_REMOTE_DOWNLOAD_UNIT_BYTES = 100 * 1024 * 1024 @@ -50,6 +54,8 @@ const QUEUE_ASSIGN_BATCH = 20 const CONTROL_TASK_PAGE_SIZE = 100 const DOWNLOADER_ACTIVE_NEXT_POLL_SECONDS = 5 const DOWNLOADER_IDLE_NEXT_POLL_SECONDS = 60 +const DOWNLOAD_TASK_TARGET_TYPE = 'download_task' +const TASK_EVENT_PAGE_SIZE = 100 const PAUSABLE_TASK_STATUSES = ['queued', 'assigned', 'downloading'] as const const CANCELABLE_TASK_STATUSES = [ @@ -88,6 +94,18 @@ const DELETE_DOWNLOADER_REQUEUE_STATUSES = [ 'canceling', ] const STALE_REQUEUE_STATUSES = ['assigned', 'downloading', 'uploading', 'interrupted'] +const LIFECYCLE_FIELDS = [ + 'resolveStartedAt', + 'resolveCompletedAt', + 'downloadCompletedAt', + 'ingestStartedAt', + 'ingestCompletedAt', + 'seedingStartedAt', + 'seedingStoppedAt', +] as const + +type LifecycleField = (typeof LIFECYCLE_FIELDS)[number] +type TaskLifecycleFields = Partial> // ─── Downloader registration / admin CRUD ─────────────────────────────────── @@ -251,6 +269,17 @@ export async function createDownloadTask( assignedAt: null, now, }) + await recordTaskActivity(deps, { + task: { + id, + orgId, + createdByUserId: userId, + displayName: input.name ?? null, + sourceUri: input.source.uri, + }, + action: 'download_task_created', + metadata: { sourceType: input.source.type, targetFolder: input.targetFolder }, + }) return deps.downloadTasks.get(orgId, id) } @@ -271,6 +300,25 @@ export function getDownloadTask(deps: DownloadsDeps, orgId: string, id: string): return deps.downloadTasks.get(orgId, id) } +export async function getDownloadTaskTimeline( + deps: DownloadsDeps, + orgId: string, + id: string, +): Promise<{ items: DownloadTaskTimelineItem[] }> { + const task = await deps.downloadTasks.getRecord(orgId, id) + const activity = await deps.activity.listByTarget({ + orgId, + targetType: DOWNLOAD_TASK_TARGET_TYPE, + targetId: id, + page: 1, + pageSize: TASK_EVENT_PAGE_SIZE, + }) + const activityItems = activity.items.map((event) => activityTimelineItem(task.id, event)) + const activityActions = new Set(activityItems.map((item) => item.action)) + const taskItems = taskLifecycleTimelineItems(task).filter((item) => !activityActions.has(item.action)) + return { items: [...activityItems, ...taskItems].sort((a, b) => Date.parse(b.time) - Date.parse(a.time)) } +} + export async function updateDownloadTask( deps: DownloadsDeps, platform: Platform, @@ -386,7 +434,8 @@ export async function updateDownloadTask( const nextFinishedAt = task.finishedAt ?? (input.status !== undefined && ['completed', 'failed', 'canceled'].includes(status) ? now : null) - await deps.downloadTasks.setFields(id, { + const lifecycleFields = taskLifecycleFields(task, currentRuntime, nextRuntime, status, now) + const fields = { status, billingAuthorizedBytes, billingChargedBytes, @@ -398,6 +447,17 @@ export async function updateDownloadTask( startedAt: task.startedAt ?? (status === 'downloading' ? now : null), finishedAt: nextFinishedAt, updatedAt: now, + ...lifecycleFields, + } + + await deps.downloadTasks.setFields(id, fields) + await recordUpdateActivity(deps, task, { + input, + status, + previousStatus: task.status, + runtime: nextRuntime, + lifecycleFields, + billingStatus, }) return deps.downloadTasks.get(task.orgId, id) @@ -442,8 +502,10 @@ export async function performDownloadTaskAction( }), updatedAt: now, }) + await recordTaskActivity(deps, { task, action: 'download_task_deleted' }) return { id, deleted: true } } + await recordTaskActivity(deps, { task, action: 'download_task_deleted' }) await deps.downloadTasks.delete(id) return { id, deleted: true } } @@ -459,6 +521,10 @@ export async function performDownloadTaskAction( runtime: serializeTaskRuntime(stoppedRuntime(task.runtime)), updatedAt: now, }) + await recordTaskActivity(deps, { + task, + action: status === 'pausing' ? 'download_task_pause_requested' : 'download_task_paused', + }) return deps.downloadTasks.get(orgId, id) } @@ -473,6 +539,7 @@ export async function performDownloadTaskAction( runtime: clearTaskRuntimeMessageJson(task.runtime), updatedAt: now, }) + await recordTaskActivity(deps, { task, action: 'download_task_resume_requested' }) return deps.downloadTasks.get(orgId, id) } @@ -492,6 +559,10 @@ export async function performDownloadTaskAction( finishedAt: status === 'canceled' ? (task.finishedAt ?? now) : task.finishedAt, updatedAt: now, }) + await recordTaskActivity(deps, { + task, + action: status === 'canceling' ? 'download_task_cancel_requested' : 'download_task_canceled', + }) return deps.downloadTasks.get(orgId, id) } @@ -511,6 +582,7 @@ export async function performDownloadTaskAction( finishedAt: null, updatedAt: now, }) + await recordTaskActivity(deps, { task, action: 'download_task_retry_requested' }) return deps.downloadTasks.get(orgId, id) } @@ -535,6 +607,7 @@ export async function performDownloadTaskAction( finishedAt: null, updatedAt: now, }) + await recordTaskActivity(deps, { task, action: 'download_task_restart_requested' }) return deps.downloadTasks.get(orgId, id) } @@ -563,7 +636,14 @@ async function claimQueuedTasksForDownloader( for (const task of tasks) { if (remaining <= 0) break if (!canDownloaderRunSource(params.capabilities, task.sourceType)) continue - if (await deps.downloadTasks.claimQueued(task.id, params.id, params.now)) remaining -= 1 + if (await deps.downloadTasks.claimQueued(task.id, params.id, params.now)) { + remaining -= 1 + await recordTaskActivity(deps, { + task, + action: 'download_task_assigned', + metadata: { downloaderId: params.id }, + }) + } } } @@ -584,14 +664,47 @@ async function recoverStaleDownloaderAssignments(deps: DownloadsDeps): Promise 0) { + const controls = await listTasksForDownloaders(deps, unreachableIds, ['canceling', 'pausing']) await deps.downloadTasks.resolveControlAssignedToMany(unreachableIds, now) + await Promise.all( + controls.map((task) => + recordTaskActivity(deps, { + task, + action: 'download_stale_control_resolved', + metadata: { previousStatus: task.status, downloaderId: task.assignedDownloaderId }, + }), + ), + ) } // Requeue in-flight work and flip status to offline only on the online→offline // transition, so already-handled tasks are not re-queued repeatedly. const staleIds = await deps.downloaders.listStaleIds(leaseCutoff) if (staleIds.length === 0) return + const requeued = await listTasksForDownloaders(deps, staleIds, STALE_REQUEUE_STATUSES) await deps.downloadTasks.requeueAssignedToMany(staleIds, STALE_REQUEUE_STATUSES, now) await deps.downloaders.markStaleOffline(staleIds, now) + await Promise.all( + requeued.map((task) => + recordTaskActivity(deps, { + task, + action: 'download_stale_requeued', + metadata: { previousStatus: task.status, downloaderId: task.assignedDownloaderId }, + }), + ), + ) +} + +async function listTasksForDownloaders( + deps: DownloadsDeps, + downloaderIds: string[], + statuses: readonly string[], +): Promise { + const pages = await Promise.all( + downloaderIds.map((downloaderId) => + deps.downloadTasks.list({ downloaderId, statuses: [...statuses], page: 1, pageSize: CONTROL_TASK_PAGE_SIZE }), + ), + ) + return pages.flatMap((page) => page.rows) } // ─── Upload-token minting ──────────────────────────────────────────────────── @@ -728,6 +841,256 @@ function clearTaskRuntimeMessage(runtime: DownloadTaskRuntime | null): DownloadT return Object.keys(rest).length > 0 ? rest : null } +function taskLifecycleFields( + task: DownloadTaskRecord, + current: DownloadTaskRuntime | null, + next: DownloadTaskRuntime | null, + status: string, + now: Date, +): TaskLifecycleFields { + const fields: TaskLifecycleFields = {} + const currentPhase = current?.phase + const nextPhase = next?.phase + + if (!task.resolveStartedAt && nextPhase === 'metadata') fields.resolveStartedAt = now + if (!task.resolveCompletedAt && currentPhase === 'metadata' && nextPhase && nextPhase !== 'metadata') { + fields.resolveCompletedAt = now + } + if (!task.downloadCompletedAt && (status === 'uploading' || status === 'completed' || nextPhase === 'uploading')) { + fields.downloadCompletedAt = now + } + if (!task.ingestStartedAt && (status === 'uploading' || nextPhase === 'uploading')) fields.ingestStartedAt = now + if (!task.ingestCompletedAt && status === 'completed') fields.ingestCompletedAt = now + if (!task.seedingStartedAt && nextPhase === 'seeding') fields.seedingStartedAt = now + if (!task.seedingStoppedAt && currentPhase === 'seeding' && nextPhase && nextPhase !== 'seeding') { + fields.seedingStoppedAt = now + } + return fields +} + +async function recordUpdateActivity( + deps: DownloadsDeps, + task: DownloadTaskRecord, + params: { + input: UpdateDownloadTaskInput + status: string + previousStatus: string + runtime: DownloadTaskRuntime | null + lifecycleFields: TaskLifecycleFields + billingStatus: string + }, +): Promise { + for (const field of LIFECYCLE_FIELDS) { + if (!params.lifecycleFields[field]) continue + await recordTaskActivity(deps, { + task, + action: lifecycleAction(field), + metadata: runtimeMetadata(params.runtime), + }) + } + + if (params.previousStatus !== params.status) { + await recordTaskActivity(deps, { + task, + action: statusAction(params.status), + metadata: { + from: params.previousStatus, + to: params.status, + ...runtimeMetadata(params.runtime), + }, + }) + } + + if (params.input.errorMessage) { + await recordTaskActivity(deps, { + task, + action: 'download_task_error', + metadata: { message: params.input.errorMessage }, + }) + } + + if (params.billingStatus === 'insufficient_credits' && task.billingStatus !== 'insufficient_credits') { + await recordTaskActivity(deps, { + task, + action: 'download_task_billing_suspended', + metadata: { reason: 'insufficient_credits' }, + }) + } +} + +function lifecycleAction(field: LifecycleField): string { + if (field === 'resolveStartedAt') return 'download_resolve_started' + if (field === 'resolveCompletedAt') return 'download_resolve_completed' + if (field === 'downloadCompletedAt') return 'download_completed' + if (field === 'ingestStartedAt') return 'download_ingest_started' + if (field === 'ingestCompletedAt') return 'download_ingest_completed' + if (field === 'seedingStartedAt') return 'download_seeding_started' + return 'download_seeding_stopped' +} + +function statusAction(status: string): string { + if (status === 'downloading') return 'download_task_started' + if (status === 'uploading') return 'download_task_ingesting' + if (status === 'completed') return 'download_task_completed' + if (status === 'failed') return 'download_task_failed' + if (status === 'canceled') return 'download_task_canceled' + if (status === 'suspended') return 'download_task_suspended' + if (status === 'paused') return 'download_task_paused' + if (status === 'pausing') return 'download_task_pause_requested' + if (status === 'canceling') return 'download_task_cancel_requested' + if (status === 'queued') return 'download_task_queued' + if (status === 'assigned') return 'download_task_assigned' + return `download_task_${status}` +} + +async function recordTaskActivity( + deps: DownloadsDeps, + input: { + task: Pick + action: string + metadata?: Record + }, +): Promise { + await deps.activity.record({ + orgId: input.task.orgId, + userId: input.task.createdByUserId, + action: input.action, + targetType: DOWNLOAD_TASK_TARGET_TYPE, + targetId: input.task.id, + targetName: taskTargetName(input.task), + metadata: input.metadata, + }) +} + +function taskTargetName(task: Pick): string { + return task.displayName || task.sourceUri +} + +function runtimeMetadata(runtime: DownloadTaskRuntime | null): Record { + return { + ...(runtime?.engine ? { engine: runtime.engine } : {}), + ...(runtime?.phase ? { phase: runtime.phase } : {}), + ...(runtime?.state ? { engineState: runtime.state } : {}), + ...(runtime?.torrent?.infoHash ? { infoHash: runtime.torrent.infoHash } : {}), + ...(runtime?.trackers ? { trackerCount: runtime.trackers.length } : {}), + ...(runtime?.torrent?.seeders !== undefined ? { seeders: runtime.torrent.seeders } : {}), + ...(runtime?.torrent?.peers !== undefined ? { peers: runtime.torrent.peers } : {}), + } +} + +function taskLifecycleTimelineItems(task: DownloadTaskRecord): DownloadTaskTimelineItem[] { + const items = [ + lifecycleTimelineItem(task, 'download_task_created', task.createdAt), + task.assignedAt && lifecycleTimelineItem(task, 'download_task_assigned', task.assignedAt), + task.startedAt && lifecycleTimelineItem(task, 'download_task_started', task.startedAt), + task.resolveStartedAt && lifecycleTimelineItem(task, 'download_resolve_started', task.resolveStartedAt), + task.resolveCompletedAt && lifecycleTimelineItem(task, 'download_resolve_completed', task.resolveCompletedAt), + task.downloadCompletedAt && lifecycleTimelineItem(task, 'download_completed', task.downloadCompletedAt), + task.ingestStartedAt && lifecycleTimelineItem(task, 'download_ingest_started', task.ingestStartedAt), + task.ingestCompletedAt && lifecycleTimelineItem(task, 'download_ingest_completed', task.ingestCompletedAt), + task.seedingStartedAt && lifecycleTimelineItem(task, 'download_seeding_started', task.seedingStartedAt), + task.seedingStoppedAt && lifecycleTimelineItem(task, 'download_seeding_stopped', task.seedingStoppedAt), + task.finishedAt && lifecycleTimelineItem(task, statusAction(task.status), task.finishedAt), + ].filter(Boolean) as DownloadTaskTimelineItem[] + return items +} + +function lifecycleTimelineItem(task: DownloadTaskRecord, action: string, time: Date): DownloadTaskTimelineItem { + return { + id: `task:${action}:${time.getTime()}`, + taskId: task.id, + time: time.toISOString(), + source: 'task', + action, + title: actionTitle(action), + detail: null, + severity: actionSeverity(action), + metadata: null, + } +} + +function activityTimelineItem(taskId: string, event: ActivityEvent): DownloadTaskTimelineItem { + const metadata = parseActivityMetadata(event.metadata) + return { + id: event.id, + taskId, + time: event.createdAt.toISOString(), + source: 'activity', + action: event.action, + title: actionTitle(event.action), + detail: actionDetail(metadata), + severity: actionSeverity(event.action), + metadata, + } +} + +function parseActivityMetadata(value: string | null): Record | null { + if (!value) return null + try { + const parsed = JSON.parse(value) + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? (parsed as Record) : null + } catch { + return null + } +} + +function actionTitle(action: string): string { + const titles: Record = { + download_task_created: 'Task created', + download_task_assigned: 'Assigned to downloader', + download_task_queued: 'Queued', + download_task_started: 'Download started', + download_task_ingesting: 'Ingesting', + download_task_completed: 'Task completed', + download_task_failed: 'Task failed', + download_task_canceled: 'Task canceled', + download_task_suspended: 'Task suspended', + download_task_paused: 'Task paused', + download_task_pause_requested: 'Pause requested', + download_task_resume_requested: 'Resume requested', + download_task_cancel_requested: 'Cancel requested', + download_task_retry_requested: 'Retry requested', + download_task_restart_requested: 'Restart requested', + download_task_deleted: 'Task deleted', + download_task_error: 'Error reported', + download_task_billing_suspended: 'Billing suspended', + download_resolve_started: 'Resolving source', + download_resolve_completed: 'Source resolved', + download_completed: 'Download completed', + download_ingest_started: 'Ingest started', + download_ingest_completed: 'Ingest completed', + download_seeding_started: 'Seeding started', + download_seeding_stopped: 'Seeding stopped', + download_stale_requeued: 'Requeued after downloader went offline', + download_stale_control_resolved: 'Resolved after downloader went offline', + } + return titles[action] ?? action.replace(/_/g, ' ') +} + +function actionDetail(metadata: Record | null): string | null { + if (!metadata) return null + if (typeof metadata.message === 'string') return metadata.message + if (typeof metadata.reason === 'string') return metadata.reason + const parts = [ + typeof metadata.engine === 'string' ? metadata.engine : null, + typeof metadata.phase === 'string' ? metadata.phase : null, + typeof metadata.infoHash === 'string' ? `infoHash ${metadata.infoHash}` : null, + typeof metadata.trackerCount === 'number' ? `${metadata.trackerCount} trackers` : null, + typeof metadata.seeders === 'number' ? `${metadata.seeders} seeders` : null, + typeof metadata.peers === 'number' ? `${metadata.peers} peers` : null, + ].filter(Boolean) + if (parts.length > 0) return parts.join(' · ') + if (typeof metadata.from === 'string' && typeof metadata.to === 'string') return `${metadata.from} -> ${metadata.to}` + return null +} + +function actionSeverity(action: string): DownloadTaskTimelineItem['severity'] { + if (action.includes('failed') || action.includes('error')) return 'error' + if (action.includes('suspended') || action.includes('offline')) return 'warning' + if (action.includes('completed')) return 'success' + return 'info' +} + function downloadTaskFromRecord(row: DownloadTaskRecord): DownloadTask { const runtime = parseTaskRuntime(row.runtime) return { @@ -767,6 +1130,13 @@ function downloadTaskFromRecord(row: DownloadTaskRecord): DownloadTask { output: row.resultObjectId ? { objectId: row.resultObjectId } : null, runtime, error: row.errorMessage ? { code: row.errorCode, message: row.errorMessage } : null, + resolveStartedAt: row.resolveStartedAt?.toISOString() ?? null, + resolveCompletedAt: row.resolveCompletedAt?.toISOString() ?? null, + downloadCompletedAt: row.downloadCompletedAt?.toISOString() ?? null, + ingestStartedAt: row.ingestStartedAt?.toISOString() ?? null, + ingestCompletedAt: row.ingestCompletedAt?.toISOString() ?? null, + seedingStartedAt: row.seedingStartedAt?.toISOString() ?? null, + seedingStoppedAt: row.seedingStoppedAt?.toISOString() ?? null, startedAt: row.startedAt?.toISOString() ?? null, finishedAt: row.finishedAt?.toISOString() ?? null, updatedAt: row.updatedAt.toISOString(), diff --git a/server/usecases/ports/activity.ts b/server/usecases/ports/activity.ts index e8f7144e..4f1cc7e0 100644 --- a/server/usecases/ports/activity.ts +++ b/server/usecases/ports/activity.ts @@ -41,6 +41,14 @@ export interface ListAdminAuditOpts { targetType?: string } +export interface ListActivityByTargetOpts { + orgId: string + targetType: string + targetId: string + page?: number + pageSize?: number +} + export interface ActivityRepo { record(event: RecordActivityInput): Promise list( @@ -50,4 +58,7 @@ export interface ActivityRepo { listAdminAudit( opts: ListAdminAuditOpts, ): Promise<{ items: AdminAuditEventWithOrg[]; total: number; page: number; pageSize: number }> + listByTarget( + opts: ListActivityByTargetOpts, + ): Promise<{ items: ActivityEvent[]; total: number; page: number; pageSize: number }> } diff --git a/server/usecases/ports/downloads.ts b/server/usecases/ports/downloads.ts index 299c4007..e5830b9e 100644 --- a/server/usecases/ports/downloads.ts +++ b/server/usecases/ports/downloads.ts @@ -68,6 +68,13 @@ export interface DownloadTaskRecord { errorMessage: string | null resultObjectId: string | null runtime: string | null + resolveStartedAt: Date | null + resolveCompletedAt: Date | null + downloadCompletedAt: Date | null + ingestStartedAt: Date | null + ingestCompletedAt: Date | null + seedingStartedAt: Date | null + seedingStoppedAt: Date | null createdAt: Date updatedAt: Date assignedAt: Date | null @@ -208,6 +215,13 @@ export interface UpdateDownloadTaskFields { errorMessage?: string | null resultObjectId?: string | null runtime?: string | null + resolveStartedAt?: Date | null + resolveCompletedAt?: Date | null + downloadCompletedAt?: Date | null + ingestStartedAt?: Date | null + ingestCompletedAt?: Date | null + seedingStartedAt?: Date | null + seedingStoppedAt?: Date | null assignedAt?: Date | null startedAt?: Date | null finishedAt?: Date | null diff --git a/shared/schemas/downloads.ts b/shared/schemas/downloads.ts index fc1d6fc1..648fb1b3 100644 --- a/shared/schemas/downloads.ts +++ b/shared/schemas/downloads.ts @@ -145,6 +145,13 @@ export const downloadTaskSchema = z message: z.string().max(1000).nullable(), }) .nullable(), + resolveStartedAt: z.string().nullable(), + resolveCompletedAt: z.string().nullable(), + downloadCompletedAt: z.string().nullable(), + ingestStartedAt: z.string().nullable(), + ingestCompletedAt: z.string().nullable(), + seedingStartedAt: z.string().nullable(), + seedingStoppedAt: z.string().nullable(), startedAt: z.string().nullable(), finishedAt: z.string().nullable(), updatedAt: z.string(), @@ -155,6 +162,30 @@ export const downloadTaskSchema = z export type DownloadTask = z.infer +export const downloadTaskTimelineItemSchema = z + .object({ + id: z.string(), + taskId: z.string(), + time: z.string(), + source: z.enum(['task', 'activity']), + action: z.string(), + title: z.string(), + detail: z.string().nullable(), + severity: z.enum(['info', 'success', 'warning', 'error']), + metadata: z.record(z.string(), z.unknown()).nullable(), + }) + .openapi('DownloadTaskTimelineItem') + +export type DownloadTaskTimelineItem = z.infer + +export const downloadTaskTimelineSchema = z + .object({ + items: z.array(downloadTaskTimelineItemSchema), + }) + .openapi('DownloadTaskTimeline') + +export type DownloadTaskTimeline = z.infer + export const downloadTaskPageSchema = z .object({ items: z.array(downloadTaskSchema), diff --git a/shared/schemas/index.ts b/shared/schemas/index.ts index 5c7bd535..78250700 100644 --- a/shared/schemas/index.ts +++ b/shared/schemas/index.ts @@ -109,6 +109,7 @@ export { downloadTaskSchema, downloadTaskStatusSchema, downloadTaskStatusUpdateSchema, + downloadTaskTimelineSchema, listDownloadTasksQuerySchema, presignObjectUploadPartsSchema, updateDownloaderCreditBillingSchema, diff --git a/shared/types/index.ts b/shared/types/index.ts index 01fe5df2..166475d5 100644 --- a/shared/types/index.ts +++ b/shared/types/index.ts @@ -240,7 +240,7 @@ export type DownloadTaskBillingState = 'none' | 'ok' | 'insufficient_credits' // shared/schemas/downloads.ts — one source of truth for the OpenAPI document, // the generated SDKs, the backend, and the frontend. The sub-interfaces below // stay as named building blocks the schema mirrors field-for-field. -export type { DownloadTask } from '../schemas/downloads' +export type { DownloadTask, DownloadTaskTimeline, DownloadTaskTimelineItem } from '../schemas/downloads' export interface DownloadTaskSpec { source: { @@ -266,6 +266,13 @@ export interface DownloadTaskExecutionStatus { output: DownloadTaskOutput | null runtime: DownloadTaskRuntime | null error: DownloadTaskError | null + resolveStartedAt: string | null + resolveCompletedAt: string | null + downloadCompletedAt: string | null + ingestStartedAt: string | null + ingestCompletedAt: string | null + seedingStartedAt: string | null + seedingStoppedAt: string | null startedAt: string | null finishedAt: string | null updatedAt: string diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 31be708f..8d14a481 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -231,7 +231,7 @@ "downloads.detail.tabs.trackers": "Trackers", "downloads.detail.tabs.peers": "Peers", "downloads.detail.tabs.files": "Files", - "downloads.detail.tabs.log": "Log", + "downloads.detail.tabs.events": "Events", "downloads.detail.noSelection": "Select a download task to inspect it", "downloads.detail.target": "Target folder", "downloads.detail.category": "Category", @@ -273,7 +273,35 @@ "downloads.detail.selected": "Selected", "downloads.detail.skipped": "Skipped", "downloads.detail.errorMessage": "Error", - "downloads.detail.noLog": "No log yet", + "downloads.detail.eventsLoading": "Loading events...", + "downloads.detail.noEvents": "No events yet", + "downloads.events.download_task_created": "Task created", + "downloads.events.download_task_assigned": "Assigned to downloader", + "downloads.events.download_task_queued": "Queued", + "downloads.events.download_task_started": "Download started", + "downloads.events.download_task_ingesting": "Ingesting", + "downloads.events.download_task_completed": "Task completed", + "downloads.events.download_task_failed": "Task failed", + "downloads.events.download_task_canceled": "Task canceled", + "downloads.events.download_task_suspended": "Task suspended", + "downloads.events.download_task_paused": "Task paused", + "downloads.events.download_task_pause_requested": "Pause requested", + "downloads.events.download_task_resume_requested": "Resume requested", + "downloads.events.download_task_cancel_requested": "Cancel requested", + "downloads.events.download_task_retry_requested": "Retry requested", + "downloads.events.download_task_restart_requested": "Restart requested", + "downloads.events.download_task_deleted": "Task deleted", + "downloads.events.download_task_error": "Error reported", + "downloads.events.download_task_billing_suspended": "Billing suspended", + "downloads.events.download_resolve_started": "Resolving source", + "downloads.events.download_resolve_completed": "Source resolved", + "downloads.events.download_completed": "Download completed", + "downloads.events.download_ingest_started": "Ingest started", + "downloads.events.download_ingest_completed": "Ingest completed", + "downloads.events.download_seeding_started": "Seeding started", + "downloads.events.download_seeding_stopped": "Seeding stopped", + "downloads.events.download_stale_requeued": "Requeued after downloader went offline", + "downloads.events.download_stale_control_resolved": "Resolved after downloader went offline", "uploadPanel.toggle": "Upload progress", "uploadPanel.title": "Uploads", "uploadPanel.empty": "No uploads yet", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 1d05365a..32a84e55 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -231,7 +231,7 @@ "downloads.detail.tabs.trackers": "Trackers", "downloads.detail.tabs.peers": "Peers", "downloads.detail.tabs.files": "文件", - "downloads.detail.tabs.log": "日志", + "downloads.detail.tabs.events": "事件", "downloads.detail.noSelection": "选择一个下载任务查看详情", "downloads.detail.target": "目标目录", "downloads.detail.category": "分类", @@ -273,7 +273,35 @@ "downloads.detail.selected": "已选择", "downloads.detail.skipped": "跳过", "downloads.detail.errorMessage": "错误", - "downloads.detail.noLog": "暂无日志", + "downloads.detail.eventsLoading": "正在加载事件...", + "downloads.detail.noEvents": "暂无事件", + "downloads.events.download_task_created": "任务已创建", + "downloads.events.download_task_assigned": "已分配给下载器", + "downloads.events.download_task_queued": "已排队", + "downloads.events.download_task_started": "下载已开始", + "downloads.events.download_task_ingesting": "正在回传", + "downloads.events.download_task_completed": "任务已完成", + "downloads.events.download_task_failed": "任务失败", + "downloads.events.download_task_canceled": "任务已取消", + "downloads.events.download_task_suspended": "任务已挂起", + "downloads.events.download_task_paused": "任务已暂停", + "downloads.events.download_task_pause_requested": "已请求暂停", + "downloads.events.download_task_resume_requested": "已请求恢复", + "downloads.events.download_task_cancel_requested": "已请求取消", + "downloads.events.download_task_retry_requested": "已请求重试", + "downloads.events.download_task_restart_requested": "已请求重启", + "downloads.events.download_task_deleted": "任务已删除", + "downloads.events.download_task_error": "错误已上报", + "downloads.events.download_task_billing_suspended": "计费挂起", + "downloads.events.download_resolve_started": "开始解析来源", + "downloads.events.download_resolve_completed": "来源解析完成", + "downloads.events.download_completed": "下载完成", + "downloads.events.download_ingest_started": "开始回传", + "downloads.events.download_ingest_completed": "回传完成", + "downloads.events.download_seeding_started": "开始做种", + "downloads.events.download_seeding_stopped": "做种停止", + "downloads.events.download_stale_requeued": "下载器离线后已重新排队", + "downloads.events.download_stale_control_resolved": "下载器离线后已处理控制状态", "uploadPanel.toggle": "上传进度", "uploadPanel.title": "上传", "uploadPanel.empty": "暂无上传任务", diff --git a/src/lib/api.test.ts b/src/lib/api.test.ts index 302aaa84..e123e7ba 100644 --- a/src/lib/api.test.ts +++ b/src/lib/api.test.ts @@ -75,6 +75,7 @@ import { listCloudProducts, listCloudStoreTargets, listDownloaders, + listDownloadTaskEvents, listDownloadTasks, listIhostApiKeys, listIhostImages, @@ -1146,6 +1147,24 @@ describe('api', () => { expect(init.body).toBe(JSON.stringify(body)) }) + it('lists download task events', async () => { + const payload = { items: [{ id: 'event-1', action: 'download_task_created' }] } + vi.mocked(fetch).mockResolvedValueOnce(makeResponse(payload)) + + const result = await listDownloadTaskEvents('task-1') + + expect(result).toEqual(payload) + const [url, init] = vi.mocked(fetch).mock.calls[0] as [string, RequestInit] + expect(url).toBe('/api/downloads/tasks/task-1/events') + expect(init.method).toBe('GET') + }) + + it('throws when listing download task events fails', async () => { + vi.mocked(fetch).mockResolvedValueOnce(makeResponse({ error: 'not found' }, false, 404)) + + await expect(listDownloadTaskEvents('missing')).rejects.toThrow('not found') + }) + it('pauses a download task via PUT /status', async () => { const payload = { id: 'task-1', status: 'paused' } vi.mocked(fetch).mockResolvedValueOnce(makeResponse(payload)) diff --git a/src/lib/api.ts b/src/lib/api.ts index 1269a9c5..2b9127c1 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -42,6 +42,7 @@ import type { CloudStoreTarget, Downloader, DownloadTask, + DownloadTaskTimeline, IhostConfigResponse, ImageHosting, InstanceInfo, @@ -339,6 +340,10 @@ export function updateDownloadTask(id: string, data: UpdateDownloadTaskInput) { return unwrap(downloadTasksApi[':id'].$patch({ param: { id }, json: data })) } +export function listDownloadTaskEvents(id: string) { + return unwrap(downloadTasksApi[':id'].events.$get({ param: { id } })) +} + export function runDownloadTaskAction(id: string, action: DownloadTaskActionInput['action']) { if (action === 'delete') { return discard(downloadTasksApi[':id'].$delete({ param: { id } })) diff --git a/src/routes/_authenticated/downloads/index.tsx b/src/routes/_authenticated/downloads/index.tsx index d0d05fcb..9a2a70d3 100644 --- a/src/routes/_authenticated/downloads/index.tsx +++ b/src/routes/_authenticated/downloads/index.tsx @@ -1,5 +1,11 @@ import { DirType } from '@shared/constants' -import type { DownloadTask, DownloadTaskAction, DownloadTaskStatus, StorageObject } from '@shared/types' +import type { + DownloadTask, + DownloadTaskAction, + DownloadTaskStatus, + DownloadTaskTimelineItem, + StorageObject, +} from '@shared/types' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { createFileRoute } from '@tanstack/react-router' import { @@ -28,6 +34,7 @@ import { FolderInput, Gauge, GripVertical, + History, Home, LinkIcon, Magnet, @@ -84,7 +91,7 @@ import { Textarea } from '@/components/ui/textarea' import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { useServerEventSubscription } from '@/hooks/useServerEvents' -import { createDownloadTask, listDownloadTasks, runDownloadTaskAction } from '@/lib/api' +import { createDownloadTask, listDownloadTaskEvents, listDownloadTasks, runDownloadTaskAction } from '@/lib/api' import { cn } from '@/lib/utils' export const Route = createFileRoute('/_authenticated/downloads/')({ @@ -110,7 +117,7 @@ const STATUS_FILTERS: Array<{ value: DownloadTaskStatus | 'all'; labelKey: strin ] type DownloadTaskDisplayStatus = DownloadTaskStatus | 'seeding' type DownloadTaskPhase = NonNullable['phase']> -type DetailTab = 'overview' | 'trackers' | 'peers' | 'files' | 'log' +type DetailTab = 'overview' | 'trackers' | 'peers' | 'files' | 'events' type PanelDragState = { startY: number; startDetailHeight: number; containerHeight: number } type PendingTaskAction = { tasks: DownloadTask[]; action: DownloadTaskAction } type DetailTableColumn = { @@ -145,7 +152,7 @@ const DETAIL_TABS: Array<{ id: DetailTab; labelKey: string; icon: ReactNode }> = { id: 'trackers', labelKey: 'downloads.detail.tabs.trackers', icon: }, { id: 'peers', labelKey: 'downloads.detail.tabs.peers', icon: }, { id: 'files', labelKey: 'downloads.detail.tabs.files', icon: }, - { id: 'log', labelKey: 'downloads.detail.tabs.log', icon: }, + { id: 'events', labelKey: 'downloads.detail.tabs.events', icon: }, ] function DownloadsPage() { @@ -1430,7 +1437,7 @@ function DownloadInspector({ {tab === 'trackers' && } {tab === 'peers' && } {tab === 'files' && } - {tab === 'log' && } + {tab === 'events' && } ) @@ -1862,85 +1869,59 @@ function countryCodeToFlag(countryCode: string) { return String.fromCodePoint(...[...countryCode].map((char) => char.charCodeAt(0) + regionalIndicatorOffset)) } -function LogPanel({ task }: { task: DownloadTask }) { +function EventsPanel({ task }: { task: DownloadTask }) { const { t } = useTranslation() - const detail = task.status.runtime - const events = [ - { - id: 'created', - tone: 'neutral', - time: formatDate(task.createdAt), - title: t('downloads.detail.createdAt'), - detail: sourceUri(task), - }, - task.status.startedAt && { - id: 'started', - tone: 'active', - time: formatDate(task.status.startedAt), - title: t('downloads.detail.startedAt'), - detail: [detail?.engine, formatPhase(detail?.phase, t)].filter(Boolean).join(' · '), - }, - detail?.message && { - id: 'runtime-message', - tone: 'warning', - time: formatDate(detail.updatedAt), - title: t('downloads.detail.statusMessage'), - detail: detail.message, - }, - task.status.error?.message && { - id: 'error', - tone: 'error', - time: formatDate(task.status.updatedAt), - title: t('downloads.detail.errorMessage'), - detail: task.status.error.message, - }, - task.status.finishedAt && { - id: 'finished', - tone: task.status.state === 'completed' ? 'success' : 'neutral', - time: formatDate(task.status.finishedAt), - title: t('downloads.detail.finishedAt'), - detail: t(`downloads.status.${task.status.state}`), - }, - ].filter(Boolean) as Array<{ - id: string - tone: 'active' | 'error' | 'neutral' | 'success' | 'warning' - time: string - title: string - detail: string - }> - - const timelineEvents = [...events].reverse() + const eventsQuery = useQuery({ + queryKey: ['download-task-events', task.id], + queryFn: () => listDownloadTaskEvents(task.id), + }) + const events = eventsQuery.data?.items ?? [] return (
- {timelineEvents.map((event, index) => ( + {events.map((event, index) => (
- - {index < timelineEvents.length - 1 && } + + {index < events.length - 1 && }
- {event.time} - {event.title} + {formatDate(event.time)} + {timelineTitle(event, t)}
- {event.detail &&
{event.detail}
} + {timelineDetail(event, t) && ( +
{timelineDetail(event, t)}
+ )}
))} - {events.length === 0 && } + {eventsQuery.isLoading && } + {!eventsQuery.isLoading && events.length === 0 && }
) } -function logEventDotClass(tone: 'active' | 'error' | 'neutral' | 'success' | 'warning') { - if (tone === 'active') return 'bg-sky-500' - if (tone === 'error') return 'bg-destructive' - if (tone === 'success') return 'bg-emerald-500' - if (tone === 'warning') return 'bg-amber-500' +function eventDotClass(severity: DownloadTaskTimelineItem['severity']) { + if (severity === 'error') return 'bg-destructive' + if (severity === 'success') return 'bg-emerald-500' + if (severity === 'warning') return 'bg-amber-500' return 'bg-muted-foreground/50' } +function timelineTitle(event: DownloadTaskTimelineItem, t: ReturnType['t']) { + const key = `downloads.events.${event.action}` + const translated = t(key) + return translated === key ? event.title : translated +} + +function timelineDetail(event: DownloadTaskTimelineItem, t: ReturnType['t']) { + if (event.action === 'download_task_started' && typeof event.metadata?.phase === 'string') { + return formatPhase(event.metadata.phase as DownloadTaskPhase, t) + } + return event.detail +} + function SourceIcon({ type }: { type: DownloadTask['spec']['source']['type'] }) { if (type === 'magnet') return if (type === 'torrent_url') return