Upgrade documents

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
Sacha Al Himdani
2025-09-16 09:51:17 +02:00
parent ce613ca453
commit 04e3eb81a7
24 changed files with 1910 additions and 523 deletions

View File

@@ -546,3 +546,27 @@ WHERE
return nil
}
func (p *Documents) BulkSoftDelete(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
UPDATE documents SET deleted_at = @deleted_at WHERE %s AND id = ANY(@document_ids)
`
q = fmt.Sprintf(q, scope.SQLFragment())
ids := make([]gid.GID, len(*p))
for i, doc := range *p {
ids[i] = doc.ID
}
args := pgx.StrictNamedArgs{
"document_ids": ids,
"deleted_at": time.Now()}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
return err
}

View File

@@ -55,5 +55,5 @@ const (
SnapshotEntityType
ContinualImprovementEntityType
ProcessingActivityEntityType
FrameworkExportEntityType
ExportJobEntityType
)

246
pkg/coredata/export_job.go Normal file
View File

@@ -0,0 +1,246 @@
package coredata
import (
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"time"
"github.com/getprobo/probo/pkg/gid"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
)
type (
ExportJob struct {
ID gid.GID `db:"id"`
Type ExportJobType `db:"type"`
Arguments json.RawMessage `db:"arguments"`
Error *string `db:"error"`
Status ExportJobStatus `db:"status"`
FileID *gid.GID `db:"file_id"`
RecipientEmail string `db:"recipient_email"`
RecipientName string `db:"recipient_name"`
CreatedAt time.Time `db:"created_at"`
StartedAt *time.Time `db:"started_at"`
CompletedAt *time.Time `db:"completed_at"`
}
ExportJobs []*ExportJob
DocumentExportArguments struct {
DocumentIDs []gid.GID `json:"document_ids"`
}
FrameworkExportArguments struct {
FrameworkID gid.GID `json:"framework_id"`
}
)
var (
ErrNoExportJobAvailable = errors.New("no export job available")
)
func (ej *ExportJob) Insert(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
INSERT INTO export_jobs (
id,
tenant_id,
type,
arguments,
status,
recipient_email,
recipient_name,
created_at
) VALUES (
@id,
@tenant_id,
@type,
@arguments,
@status,
@recipient_email,
@recipient_name,
@created_at
)`
args := pgx.StrictNamedArgs{
"id": ej.ID,
"tenant_id": scope.GetTenantID(),
"type": ej.Type,
"arguments": ej.Arguments,
"status": ej.Status,
"recipient_email": ej.RecipientEmail,
"recipient_name": ej.RecipientName,
"created_at": ej.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err
}
func (ej *ExportJob) Update(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
UPDATE
export_jobs
SET
status = @status,
error = @error,
file_id = @file_id,
started_at = @started_at,
completed_at = @completed_at
WHERE
%s
AND id = @id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"status": ej.Status,
"error": ej.Error,
"file_id": ej.FileID,
"started_at": ej.StartedAt,
"completed_at": ej.CompletedAt,
"id": ej.ID,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
return err
}
func (ej *ExportJob) LoadByID(
ctx context.Context,
conn pg.Conn,
scope Scoper,
id gid.GID,
) error {
q := `
SELECT
id,
type,
arguments,
error,
status,
file_id,
recipient_email,
recipient_name,
created_at,
started_at,
completed_at
FROM
export_jobs
WHERE
%s
AND id = @id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{"id": id}
maps.Copy(args, scope.SQLArguments())
rows, err := conn.Query(ctx, q, args)
if err != nil {
return err
}
ej2, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ExportJob])
if err != nil {
return fmt.Errorf("cannot collect export job: %w", err)
}
*ej = ej2
return nil
}
func (ej *ExportJob) LoadNextPendingForUpdateSkipLocked(
ctx context.Context,
conn pg.Conn,
) error {
q := `
SELECT
id,
type,
arguments,
error,
status,
file_id,
recipient_email,
recipient_name,
created_at,
started_at,
completed_at
FROM
export_jobs
WHERE
status = @status
ORDER BY
created_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
`
args := pgx.StrictNamedArgs{
"status": ExportJobStatusPending,
}
rows, err := conn.Query(ctx, q, args)
if err != nil {
return err
}
ej2, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[ExportJob])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrNoExportJobAvailable
}
return fmt.Errorf("cannot collect export job: %w", err)
}
*ej = ej2
return nil
}
func (ej *ExportJob) GetDocumentExportArguments() (*DocumentExportArguments, error) {
if ej.Type != ExportJobTypeDocument {
return nil, fmt.Errorf("export job is not a document export")
}
var args DocumentExportArguments
if err := json.Unmarshal(ej.Arguments, &args); err != nil {
return nil, fmt.Errorf("cannot unmarshal document export arguments: %w", err)
}
return &args, nil
}
func (ej *ExportJob) GetFrameworkExportArguments() (*FrameworkExportArguments, error) {
if ej.Type != ExportJobTypeFramework {
return nil, fmt.Errorf("export job is not a framework export")
}
var args FrameworkExportArguments
if err := json.Unmarshal(ej.Arguments, &args); err != nil {
return nil, fmt.Errorf("cannot unmarshal framework export arguments: %w", err)
}
return &args, nil
}
func (ej *ExportJob) GetDocumentIDs() ([]gid.GID, error) {
args, err := ej.GetDocumentExportArguments()
if err != nil {
return nil, err
}
return args.DocumentIDs, nil
}
func (ej *ExportJob) GetFrameworkID() (gid.GID, error) {
args, err := ej.GetFrameworkExportArguments()
if err != nil {
return gid.GID{}, err
}
return args.FrameworkID, nil
}

View File

@@ -0,0 +1,51 @@
package coredata
import (
"database/sql/driver"
"fmt"
)
type (
ExportJobStatus string
)
const (
ExportJobStatusPending ExportJobStatus = "PENDING"
ExportJobStatusProcessing ExportJobStatus = "PROCESSING"
ExportJobStatusCompleted ExportJobStatus = "COMPLETED"
ExportJobStatusFailed ExportJobStatus = "FAILED"
)
func (ejs ExportJobStatus) String() string {
return string(ejs)
}
func (ejs *ExportJobStatus) Scan(value any) error {
var s string
switch v := value.(type) {
case string:
s = v
case []byte:
s = string(v)
default:
return fmt.Errorf("unsupported type for ExportJobStatus: %T", value)
}
switch s {
case ExportJobStatusPending.String():
*ejs = ExportJobStatusPending
case ExportJobStatusProcessing.String():
*ejs = ExportJobStatusProcessing
case ExportJobStatusCompleted.String():
*ejs = ExportJobStatusCompleted
case ExportJobStatusFailed.String():
*ejs = ExportJobStatusFailed
default:
return fmt.Errorf("invalid ExportJobStatus value: %q", s)
}
return nil
}
func (ejs ExportJobStatus) Value() (driver.Value, error) {
return ejs.String(), nil
}

View File

@@ -0,0 +1,45 @@
package coredata
import (
"database/sql/driver"
"fmt"
)
type (
ExportJobType string
)
const (
ExportJobTypeFramework ExportJobType = "FRAMEWORK"
ExportJobTypeDocument ExportJobType = "DOCUMENT"
)
func (ejt ExportJobType) String() string {
return string(ejt)
}
func (ejt *ExportJobType) Scan(value any) error {
var s string
switch v := value.(type) {
case string:
s = v
case []byte:
s = string(v)
default:
return fmt.Errorf("unsupported type for ExportJobType: %T", value)
}
switch s {
case ExportJobTypeFramework.String():
*ejt = ExportJobTypeFramework
case ExportJobTypeDocument.String():
*ejt = ExportJobTypeDocument
default:
return fmt.Errorf("invalid ExportJobType value: %q", s)
}
return nil
}
func (ejt ExportJobType) Value() (driver.Value, error) {
return ejt.String(), nil
}

View File

@@ -1,172 +0,0 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"context"
"errors"
"fmt"
"maps"
"time"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/page"
"github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
)
type (
FrameworkExport struct {
ID gid.GID `db:"id"`
FrameworkID gid.GID `db:"framework_id"`
RecipientEmail string `db:"recipient_email"`
RecipientName string `db:"recipient_name"`
Status FrameworkExportStatus `db:"status"`
FileID *gid.GID `db:"file_id"`
CreatedAt time.Time `db:"created_at"`
StartedAt *time.Time `db:"started_at"`
CompletedAt *time.Time `db:"completed_at"`
}
FrameworkExports []*FrameworkExport
)
var (
ErrNoFrameworkExportAvailable = errors.New("no framework export available")
)
func (fe FrameworkExport) CursorKey(orderBy FrameworkExportOrderField) page.CursorKey {
switch orderBy {
case FrameworkExportOrderFieldCreatedAt:
return page.NewCursorKey(fe.ID, fe.CreatedAt)
}
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
}
func (fe *FrameworkExport) Insert(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
INSERT INTO framework_exports (
id,
tenant_id,
framework_id,
recipient_email,
recipient_name,
status,
created_at
) VALUES (
@id,
@tenant_id,
@framework_id,
@recipient_email,
@recipient_name,
@status,
@created_at
)`
args := pgx.StrictNamedArgs{
"id": fe.ID,
"tenant_id": scope.GetTenantID(),
"framework_id": fe.FrameworkID,
"recipient_email": fe.RecipientEmail,
"recipient_name": fe.RecipientName,
"status": fe.Status,
"created_at": fe.CreatedAt,
}
_, err := conn.Exec(ctx, q, args)
return err
}
func (fe *FrameworkExport) Update(
ctx context.Context,
conn pg.Conn,
scope Scoper,
) error {
q := `
UPDATE
framework_exports
SET
status = @status,
file_id = @file_id,
started_at = @started_at,
completed_at = @completed_at
WHERE
%s
AND id = @id
`
q = fmt.Sprintf(q, scope.SQLFragment())
args := pgx.StrictNamedArgs{
"status": fe.Status,
"file_id": fe.FileID,
"started_at": fe.StartedAt,
"completed_at": fe.CompletedAt,
"id": fe.ID,
}
maps.Copy(args, scope.SQLArguments())
_, err := conn.Exec(ctx, q, args)
return err
}
func (fe *FrameworkExport) LoadNextPendingForUpdateSkipLocked(
ctx context.Context,
conn pg.Conn,
) error {
q := `
SELECT
id,
framework_id,
recipient_email,
recipient_name,
status,
file_id,
created_at,
started_at,
completed_at
FROM
framework_exports
WHERE
status = @status
ORDER BY
created_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
`
args := pgx.StrictNamedArgs{"status": FrameworkExportStatusPending}
rows, err := conn.Query(ctx, q, args)
if err != nil {
return err
}
fe2, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[FrameworkExport])
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return ErrNoFrameworkExportAvailable
}
return fmt.Errorf("cannot collect framework export: %w", err)
}
*fe = fe2
return nil
}

View File

@@ -1,40 +0,0 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
type (
FrameworkExportOrderField string
)
const (
FrameworkExportOrderFieldCreatedAt FrameworkExportOrderField = "CREATED_AT"
)
func (p FrameworkExportOrderField) Column() string {
return string(p)
}
func (p FrameworkExportOrderField) String() string {
return string(p)
}
func (p FrameworkExportOrderField) MarshalText() ([]byte, error) {
return []byte(p.String()), nil
}
func (p *FrameworkExportOrderField) UnmarshalText(text []byte) error {
*p = FrameworkExportOrderField(text)
return nil
}

View File

@@ -1,86 +0,0 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package coredata
import (
"database/sql/driver"
"fmt"
)
type (
FrameworkExportStatus string
)
const (
FrameworkExportStatusPending FrameworkExportStatus = "pending"
FrameworkExportStatusProcessing FrameworkExportStatus = "processing"
FrameworkExportStatusCompleted FrameworkExportStatus = "completed"
FrameworkExportStatusFailed FrameworkExportStatus = "failed"
)
func (pvs FrameworkExportStatus) MarshalText() ([]byte, error) {
return []byte(pvs.String()), nil
}
func (pvs *FrameworkExportStatus) UnmarshalText(data []byte) error {
val := string(data)
switch val {
case FrameworkExportStatusPending.String():
*pvs = FrameworkExportStatusPending
case FrameworkExportStatusProcessing.String():
*pvs = FrameworkExportStatusProcessing
case FrameworkExportStatusCompleted.String():
*pvs = FrameworkExportStatusCompleted
case FrameworkExportStatusFailed.String():
*pvs = FrameworkExportStatusFailed
default:
return fmt.Errorf("invalid FrameworkExportStatus value: %q", val)
}
return nil
}
func (pvs FrameworkExportStatus) String() string {
var val string
switch pvs {
case FrameworkExportStatusPending:
val = "pending"
case FrameworkExportStatusProcessing:
val = "processing"
case FrameworkExportStatusCompleted:
val = "completed"
case FrameworkExportStatusFailed:
val = "failed"
default:
panic(fmt.Errorf("invalid FrameworkExportStatus value: %q", string(pvs)))
}
return val
}
func (pvs *FrameworkExportStatus) Scan(value any) error {
val, ok := value.(string)
if !ok {
return fmt.Errorf("invalid scan source for FrameworkExportStatus, expected string got %T", value)
}
return pvs.UnmarshalText([]byte(val))
}
func (pvs FrameworkExportStatus) Value() (driver.Value, error) {
return pvs.String(), nil
}

View File

@@ -0,0 +1,34 @@
CREATE TYPE export_jobs_status AS ENUM (
'PENDING',
'PROCESSING',
'COMPLETED',
'FAILED'
);
CREATE TYPE export_jobs_type AS ENUM (
'FRAMEWORK',
'DOCUMENT'
);
CREATE TABLE export_jobs (
id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
type export_jobs_type NOT NULL,
arguments JSONB NOT NULL,
error TEXT,
status export_jobs_status NOT NULL,
file_id TEXT,
recipient_email TEXT NOT NULL,
recipient_name TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL,
started_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE
);
ALTER TABLE export_jobs ADD CONSTRAINT export_jobs_file_id_fkey
FOREIGN KEY (file_id)
REFERENCES files(id)
ON UPDATE CASCADE
ON DELETE SET NULL;
DROP TABLE framework_exports;

View File

@@ -1,12 +1,19 @@
package probo
import (
"archive/zip"
"context"
"encoding/json"
"fmt"
"io"
"net/url"
"os"
"regexp"
"strings"
"time"
"unicode"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/docgen"
"github.com/getprobo/probo/pkg/gid"
@@ -14,7 +21,9 @@ import (
"github.com/getprobo/probo/pkg/page"
"github.com/getprobo/probo/pkg/statelesstoken"
"github.com/jackc/pgx/v5"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.gearno.de/x/ref"
)
type (
@@ -68,6 +77,22 @@ type (
const (
TokenTypeSigningRequest = "signing_request"
documentExportEmailExpiresIn = 24 * time.Hour
documentExportEmailSubject = "Your document export is ready"
documentExportEmailBody = `
Your document export has been completed successfully.
You can download the export using the link below:
[1] %s
This link will expire in 24 hours.`
maxFilenameLength = 200
)
var (
invalidFilenameChars = regexp.MustCompile(`[<>:"/\\|?*\x00-\x1f\x7f]`)
)
func (e ErrSignatureNotCancellable) Error() string {
@@ -814,6 +839,76 @@ func (s *DocumentService) SoftDelete(
)
}
func (s *DocumentService) BulkSoftDelete(
ctx context.Context,
documentIDs []gid.GID,
) error {
documents := coredata.Documents{}
for _, documentID := range documentIDs {
documents = append(documents, &coredata.Document{ID: documentID})
}
return s.svc.pg.WithConn(
ctx,
func(conn pg.Conn) error {
return documents.BulkSoftDelete(ctx, conn, s.svc.scope)
},
)
}
func (s *DocumentService) RequestExport(
ctx context.Context,
documentIDs []gid.GID,
recipientEmail string,
recipientName string,
) (*coredata.ExportJob, error) {
var exportJobID gid.GID
exportJob := &coredata.ExportJob{}
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
for _, documentID := range documentIDs {
document := &coredata.Document{}
if err := document.LoadByID(ctx, conn, s.svc.scope, documentID); err != nil {
return fmt.Errorf("cannot load document %q: %w", documentID, err)
}
}
now := time.Now()
exportJobID = gid.New(s.svc.scope.GetTenantID(), coredata.ExportJobEntityType)
args := coredata.DocumentExportArguments{
DocumentIDs: documentIDs,
}
argsJSON, err := json.Marshal(args)
if err != nil {
return fmt.Errorf("cannot marshal document export arguments: %w", err)
}
exportJob = &coredata.ExportJob{
ID: exportJobID,
Type: coredata.ExportJobTypeDocument,
Arguments: argsJSON,
Status: coredata.ExportJobStatusPending,
RecipientEmail: recipientEmail,
RecipientName: recipientName,
CreatedAt: now,
}
if err := exportJob.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert export job: %w", err)
}
return nil
})
if err != nil {
return nil, err
}
return exportJob, nil
}
func (s *DocumentService) ListVersions(
ctx context.Context,
documentID gid.GID,
@@ -1118,6 +1213,97 @@ func (s *DocumentService) ExportPDF(
return data, nil
}
func (s *DocumentService) BuildAndUploadExport(ctx context.Context, exportJobID gid.GID) (*coredata.ExportJob, error) {
exportJob := &coredata.ExportJob{}
err := s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
if err := exportJob.LoadByID(ctx, tx, s.svc.scope, exportJobID); err != nil {
return fmt.Errorf("cannot load export job: %w", err)
}
documentIDs, err := exportJob.GetDocumentIDs()
if err != nil {
return fmt.Errorf("cannot get document IDs: %w", err)
}
tempDir := os.TempDir()
tempFile, err := os.CreateTemp(tempDir, "probo-document-export-*.zip")
if err != nil {
return fmt.Errorf("cannot create temp file: %w", err)
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
err = s.Export(ctx, documentIDs, tempFile)
if err != nil {
return fmt.Errorf("cannot export documents: %w", err)
}
uuid, err := uuid.NewV4()
if err != nil {
return fmt.Errorf("cannot generate uuid: %w", err)
}
if _, err := tempFile.Seek(0, 0); err != nil {
return fmt.Errorf("cannot seek temp file: %w", err)
}
fileInfo, err := tempFile.Stat()
if err != nil {
return fmt.Errorf("cannot stat temp file: %w", err)
}
_, err = s.svc.s3.PutObject(
ctx,
&s3.PutObjectInput{
Bucket: ref.Ref(s.svc.bucket),
Key: ref.Ref(uuid.String()),
Body: tempFile,
ContentLength: ref.Ref(fileInfo.Size()),
ContentType: ref.Ref("application/zip"),
Metadata: map[string]string{
"type": "document-export",
"export-job-id": exportJob.ID.String(),
},
},
)
if err != nil {
return fmt.Errorf("cannot upload file to S3: %w", err)
}
now := time.Now()
file := coredata.File{
ID: gid.New(exportJob.ID.TenantID(), coredata.FileEntityType),
BucketName: s.svc.bucket,
MimeType: "application/zip",
FileName: fmt.Sprintf("Documents Export %s.zip", now.Format("2006-01-02")),
FileKey: uuid.String(),
FileSize: int(fileInfo.Size()),
CreatedAt: now,
UpdatedAt: now,
}
if err := file.Insert(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert file: %w", err)
}
exportJob.FileID = &file.ID
if err := exportJob.Update(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot update export job: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return exportJob, nil
}
func exportDocumentPDF(
ctx context.Context,
html2pdfConverter *html2pdf.Converter,
@@ -1240,3 +1426,146 @@ func exportDocumentPDF(
}
return pdfData, nil
}
func (s *DocumentService) Export(
ctx context.Context,
documentIDs []gid.GID,
file io.Writer,
) (err error) {
archive := zip.NewWriter(file)
defer func() {
if closeErr := archive.Close(); closeErr != nil && err == nil {
err = fmt.Errorf("cannot close archive: %w", closeErr)
}
}()
return s.svc.pg.WithTx(
ctx,
func(conn pg.Conn) error {
for i, documentID := range documentIDs {
document := &coredata.Document{}
if err := document.LoadByID(ctx, conn, s.svc.scope, documentID); err != nil {
return fmt.Errorf("cannot load document %q: %w", documentID, err)
}
documentVersion := &coredata.DocumentVersion{}
if err := documentVersion.LoadLatestVersion(ctx, conn, s.svc.scope, documentID); err != nil {
return fmt.Errorf("cannot load document version for %q: %w", documentID, err)
}
exportedPDF, err := exportDocumentPDF(
ctx,
s.html2pdfConverter,
conn,
s.svc.scope,
documentVersion.ID,
)
if err != nil {
return fmt.Errorf("cannot export document PDF for %q: %w", documentID, err)
}
filename := fmt.Sprintf("%d_%s.pdf", i+1, sanitizeFilename(document.Title))
w, err := archive.Create(filename)
if err != nil {
return fmt.Errorf("cannot create document in archive: %w", err)
}
_, err = w.Write(exportedPDF)
if err != nil {
return fmt.Errorf("cannot write document to archive: %w", err)
}
}
return nil
},
)
}
func (s *DocumentService) SendExportEmail(
ctx context.Context,
fileID gid.GID,
recipientName string,
recipientEmail string,
) error {
return s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
file := &coredata.File{}
if err := file.LoadByID(ctx, tx, s.svc.scope, fileID); err != nil {
return fmt.Errorf("cannot load file: %w", err)
}
downloadURL, err := s.GenerateDocumentExportDownloadURL(ctx, file)
if err != nil {
return fmt.Errorf("cannot generate download URL: %w", err)
}
email := coredata.NewEmail(
recipientName,
recipientEmail,
documentExportEmailSubject,
fmt.Sprintf(documentExportEmailBody, downloadURL),
)
if err := email.Insert(ctx, tx); err != nil {
return fmt.Errorf("cannot insert email: %w", err)
}
return nil
},
)
}
func (s *DocumentService) GenerateDocumentExportDownloadURL(
ctx context.Context,
file *coredata.File,
) (string, error) {
presignClient := s3.NewPresignClient(s.svc.s3)
presignedReq, err := presignClient.PresignGetObject(
ctx,
&s3.GetObjectInput{
Bucket: ref.Ref(s.svc.bucket),
Key: ref.Ref(file.FileKey),
ResponseCacheControl: ref.Ref("max-age=3600, public"),
ResponseContentType: ref.Ref(file.MimeType),
ResponseContentDisposition: ref.Ref(fmt.Sprintf("attachment; filename=\"%s\"", file.FileName)),
},
func(opts *s3.PresignOptions) {
opts.Expires = documentExportEmailExpiresIn
},
)
if err != nil {
return "", fmt.Errorf("cannot presign GetObject request: %w", err)
}
return presignedReq.URL, nil
}
func sanitizeFilename(title string) string {
if title == "" {
return "Untitled"
}
sanitized := invalidFilenameChars.ReplaceAllString(title, "_")
sanitized = strings.TrimFunc(sanitized, func(r rune) bool {
return unicode.IsSpace(r) || r == '.'
})
sanitized = regexp.MustCompile(`[\s_]+`).ReplaceAllString(sanitized, "_")
if sanitized == "" || sanitized == "_" {
sanitized = "Untitled"
}
if len(sanitized) > maxFilenameLength-20 {
sanitized = sanitized[:maxFilenameLength-20]
sanitized = strings.TrimFunc(sanitized, func(r rune) bool {
return r == unicode.ReplacementChar
})
}
return sanitized
}

View File

@@ -17,8 +17,10 @@ package probo
import (
"archive/zip"
"context"
"encoding/json"
"fmt"
"io"
"os"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
@@ -29,6 +31,7 @@ import (
"github.com/getprobo/probo/pkg/page"
"github.com/getprobo/probo/pkg/slug"
"github.com/getprobo/probo/pkg/soagen"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.gearno.de/x/ref"
)
@@ -82,8 +85,9 @@ func (s FrameworkService) RequestExport(
frameworkID gid.GID,
recipientEmail string,
recipientName string,
) (error, *coredata.FrameworkExport) {
frameworkExport := &coredata.FrameworkExport{}
) (error, *coredata.ExportJob) {
var exportJobID gid.GID
exportJob := &coredata.ExportJob{}
err := s.svc.pg.WithTx(ctx, func(conn pg.Conn) error {
framework := &coredata.Framework{}
@@ -92,18 +96,28 @@ func (s FrameworkService) RequestExport(
}
now := time.Now()
exportJobID = gid.New(s.svc.scope.GetTenantID(), coredata.ExportJobEntityType)
frameworkExport = &coredata.FrameworkExport{
ID: gid.New(framework.ID.TenantID(), coredata.FrameworkExportEntityType),
FrameworkID: frameworkID,
Status: coredata.FrameworkExportStatusPending,
args := coredata.FrameworkExportArguments{
FrameworkID: frameworkID,
}
argsJSON, err := json.Marshal(args)
if err != nil {
return fmt.Errorf("cannot marshal framework export arguments: %w", err)
}
exportJob = &coredata.ExportJob{
ID: exportJobID,
Type: coredata.ExportJobTypeFramework,
Arguments: argsJSON,
Status: coredata.ExportJobStatusPending,
RecipientEmail: recipientEmail,
RecipientName: recipientName,
CreatedAt: now,
}
if err := frameworkExport.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert framework export: %w", err)
if err := exportJob.Insert(ctx, conn, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert export job: %w", err)
}
return nil
@@ -113,7 +127,7 @@ func (s FrameworkService) RequestExport(
return err, nil
}
return nil, frameworkExport
return nil, exportJob
}
func (s FrameworkService) Export(
@@ -675,7 +689,7 @@ func (s FrameworkService) StateOfApplicability(ctx context.Context, frameworkID
return output, nil
}
func (s FrameworkService) SendFrameworkExportEmail(
func (s FrameworkService) SendExportEmail(
ctx context.Context,
fileID gid.GID,
recipientName string,
@@ -736,3 +750,99 @@ func (s FrameworkService) GenerateFrameworkExportDownloadURL(
return presignedReq.URL, nil
}
func (s *FrameworkService) BuildAndUploadExport(ctx context.Context, exportJobID gid.GID) (*coredata.ExportJob, error) {
exportJob := &coredata.ExportJob{}
err := s.svc.pg.WithTx(
ctx,
func(tx pg.Conn) error {
if err := exportJob.LoadByID(ctx, tx, s.svc.scope, exportJobID); err != nil {
return fmt.Errorf("cannot load export job: %w", err)
}
frameworkID, err := exportJob.GetFrameworkID()
if err != nil {
return fmt.Errorf("cannot get framework ID: %w", err)
}
framework := &coredata.Framework{}
if err := framework.LoadByID(ctx, tx, s.svc.scope, frameworkID); err != nil {
return fmt.Errorf("cannot load framework: %w", err)
}
tempDir := os.TempDir()
tempFile, err := os.CreateTemp(tempDir, "probo-framework-export-*.zip")
if err != nil {
return fmt.Errorf("cannot create temp file: %w", err)
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
err = s.Export(ctx, frameworkID, tempFile)
if err != nil {
return fmt.Errorf("cannot export framework: %w", err)
}
uuid, err := uuid.NewV4()
if err != nil {
return fmt.Errorf("cannot generate uuid: %w", err)
}
if _, err := tempFile.Seek(0, 0); err != nil {
return fmt.Errorf("cannot seek temp file: %w", err)
}
fileInfo, err := tempFile.Stat()
if err != nil {
return fmt.Errorf("cannot stat temp file: %w", err)
}
_, err = s.svc.s3.PutObject(
ctx,
&s3.PutObjectInput{
Bucket: ref.Ref(s.svc.bucket),
Key: ref.Ref(uuid.String()),
Body: tempFile,
ContentLength: ref.Ref(fileInfo.Size()),
ContentType: ref.Ref("application/zip"),
Metadata: map[string]string{
"type": "framework-export",
"export-job-id": exportJob.ID.String(),
},
},
)
if err != nil {
return fmt.Errorf("cannot upload file to S3: %w", err)
}
now := time.Now()
file := coredata.File{
ID: gid.New(exportJob.ID.TenantID(), coredata.FileEntityType),
BucketName: s.svc.bucket,
MimeType: "application/zip",
FileName: fmt.Sprintf("Framework Export %s.zip", now.Format("2006-01-02")),
FileKey: uuid.String(),
FileSize: int(fileInfo.Size()),
CreatedAt: now,
UpdatedAt: now,
}
if err := file.Insert(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot insert file: %w", err)
}
exportJob.FileID = &file.ID
if err := exportJob.Update(ctx, tx, s.svc.scope); err != nil {
return fmt.Errorf("cannot update export job: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return exportJob, nil
}

View File

@@ -17,7 +17,6 @@ package probo
import (
"context"
"fmt"
"os"
"time"
"github.com/aws/aws-sdk-go-v2/service/s3"
@@ -28,11 +27,15 @@ import (
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/html2pdf"
"github.com/getprobo/probo/pkg/usrmgr"
"go.gearno.de/crypto/uuid"
"go.gearno.de/kit/pg"
"go.gearno.de/x/ref"
)
type ExportService interface {
BuildAndUploadExport(ctx context.Context, exportJobID gid.GID) (*coredata.ExportJob, error)
SendExportEmail(ctx context.Context, fileID gid.GID, recipientName, recipientEmail string) error
}
type (
TrustConfig struct {
TokenSecret string
@@ -191,170 +194,100 @@ func (s *Service) WithTenant(tenantID gid.TenantID) *TenantService {
return tenantService
}
func (s *Service) ExportFrameworkJob(ctx context.Context) error {
fe, scope, err := s.lockExport(ctx)
func (s *Service) ExportJob(ctx context.Context) error {
exportJob, err := s.lockExportJob(ctx)
if err != nil {
return fmt.Errorf("cannot lock framework export: %w", err)
return fmt.Errorf("cannot lock export job: %w", err)
}
fe, buildErr := s.buildAndUploadExport(ctx, scope, fe)
tenantService := s.WithTenant(exportJob.ID.TenantID())
var exportService ExportService
switch exportJob.Type {
case coredata.ExportJobTypeFramework:
exportService = tenantService.Frameworks
case coredata.ExportJobTypeDocument:
exportService = tenantService.Documents
default:
unknownTypeErr := fmt.Errorf("unknown export job type: %q", exportJob.Type)
if err := s.commitFailedExport(ctx, exportJob, unknownTypeErr); err != nil {
return fmt.Errorf("unknown export job type %q, and cannot commit failed export: %w", exportJob.Type, err)
}
return unknownTypeErr
}
exportJob, buildErr := exportService.BuildAndUploadExport(ctx, exportJob.ID)
if buildErr != nil {
if err := s.commitFailedExport(ctx, scope, fe); err != nil {
if err := s.commitFailedExport(ctx, exportJob, buildErr); err != nil {
return fmt.Errorf(
"cannot build and upload framework export: %w, and cannot commit failed export: %w",
"cannot build and upload %s export: %w, and cannot commit failed export: %w",
exportJob.Type,
buildErr,
err,
)
}
return fmt.Errorf("cannot build and upload framework export: %w", buildErr)
return fmt.Errorf("cannot build and upload %s export: %w", exportJob.Type, buildErr)
}
tenantService := s.WithTenant(scope.GetTenantID())
if emailErr := tenantService.Frameworks.SendFrameworkExportEmail(ctx, *fe.FileID, fe.RecipientName, fe.RecipientEmail); emailErr != nil {
if err := s.commitFailedExport(ctx, scope, fe); err != nil {
if emailErr := exportService.SendExportEmail(ctx, *exportJob.FileID, exportJob.RecipientName, exportJob.RecipientEmail); emailErr != nil {
if err := s.commitFailedExport(ctx, exportJob, emailErr); err != nil {
return fmt.Errorf(
"cannot send completion email: %w, and cannot commit failed export: %w",
emailErr,
err,
)
}
return fmt.Errorf("cannot send completion email: %w", emailErr)
}
if err := s.commitSuccessfulExport(ctx, scope, fe); err != nil {
return fmt.Errorf("cannot commit successful export: %w", err)
if err := s.commitSuccessfulExport(ctx, exportJob); err != nil {
return fmt.Errorf("cannot commit successful %s export: %w", exportJob.Type, err)
}
return nil
}
func (s *Service) lockExport(ctx context.Context) (*coredata.FrameworkExport, coredata.Scoper, error) {
fe := &coredata.FrameworkExport{}
func (s *Service) lockExportJob(ctx context.Context) (*coredata.ExportJob, error) {
exportJob := &coredata.ExportJob{}
var scope coredata.Scoper
err := s.pg.WithTx(ctx,
func(tx pg.Conn) error {
if err := fe.LoadNextPendingForUpdateSkipLocked(ctx, tx); err != nil {
return fmt.Errorf("cannot load next pending framework export: %w", err)
if err := exportJob.LoadNextPendingForUpdateSkipLocked(ctx, tx); err != nil {
return fmt.Errorf("cannot load next pending export job: %w", err)
}
scope = coredata.NewScope(fe.ID.TenantID())
scope = coredata.NewScope(exportJob.ID.TenantID())
fe.Status = coredata.FrameworkExportStatusProcessing
fe.StartedAt = ref.Ref(time.Now())
if err := fe.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update framework export: %w", err)
exportJob.Status = coredata.ExportJobStatusProcessing
exportJob.StartedAt = ref.Ref(time.Now())
if err := exportJob.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update %s export job: %w", exportJob.Type, err)
}
return nil
},
)
if err != nil {
return nil, nil, fmt.Errorf("cannot lock framework export: %w", err)
return nil, fmt.Errorf("cannot lock export job: %w", err)
}
return fe, scope, nil
return exportJob, nil
}
func (s *Service) buildAndUploadExport(ctx context.Context, scope coredata.Scoper, fe *coredata.FrameworkExport) (*coredata.FrameworkExport, error) {
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
framework := &coredata.Framework{}
if err := framework.LoadByID(ctx, tx, scope, fe.FrameworkID); err != nil {
return fmt.Errorf("cannot load framework: %w", err)
}
tempDir := os.TempDir()
tempFile, err := os.CreateTemp(tempDir, "probo-framework-export-*.zip")
if err != nil {
return fmt.Errorf("cannot create temp file: %w", err)
}
defer tempFile.Close()
defer os.Remove(tempFile.Name())
tenantService := s.WithTenant(scope.GetTenantID())
err = tenantService.Frameworks.Export(ctx, fe.FrameworkID, tempFile)
if err != nil {
return fmt.Errorf("cannot export framework: %w", err)
}
uuid, err := uuid.NewV4()
if err != nil {
return fmt.Errorf("cannot generate uuid: %w", err)
}
if _, err := tempFile.Seek(0, 0); err != nil {
return fmt.Errorf("cannot seek temp file: %w", err)
}
fileInfo, err := tempFile.Stat()
if err != nil {
return fmt.Errorf("cannot stat temp file: %w", err)
}
_, err = s.s3.PutObject(
ctx,
&s3.PutObjectInput{
Bucket: ref.Ref(s.bucket),
Key: ref.Ref(uuid.String()),
Body: tempFile,
ContentLength: ref.Ref(fileInfo.Size()),
ContentType: ref.Ref("application/zip"),
Metadata: map[string]string{
"framework-id": framework.ID.String(),
"framework-export-id": fe.ID.String(),
},
},
)
if err != nil {
return fmt.Errorf("cannot upload file to S3: %w", err)
}
now := time.Now()
file := coredata.File{
ID: gid.New(fe.ID.TenantID(), coredata.FileEntityType),
BucketName: s.bucket,
MimeType: "application/zip",
FileName: fmt.Sprintf("%s Archive %s.zip", framework.Name, now.Format("2006-01-02")),
FileKey: uuid.String(),
FileSize: int(fileInfo.Size()),
CreatedAt: now,
UpdatedAt: now,
}
if err := file.Insert(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot insert file: %w", err)
}
fe.FileID = &file.ID
if err := fe.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update framework export: %w", err)
}
return nil
},
)
if err != nil {
return fe, fmt.Errorf("cannot build and upload export: %w", err)
}
return fe, nil
}
func (s *Service) commitFailedExport(ctx context.Context, scope coredata.Scoper, fe *coredata.FrameworkExport) error {
fe.CompletedAt = ref.Ref(time.Now())
fe.Status = coredata.FrameworkExportStatusFailed
func (s *Service) commitFailedExport(ctx context.Context, exportJob *coredata.ExportJob, failureErr error) error {
exportJob.CompletedAt = ref.Ref(time.Now())
exportJob.Status = coredata.ExportJobStatusFailed
errorMsg := failureErr.Error()
exportJob.Error = &errorMsg
return s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := fe.Update(ctx, conn, scope); err != nil {
return fmt.Errorf("cannot update framework export: %w", err)
scope := coredata.NewScope(exportJob.ID.TenantID())
if err := exportJob.Update(ctx, conn, scope); err != nil {
return fmt.Errorf("cannot update %s export job: %w", exportJob.Type, err)
}
return nil
@@ -362,15 +295,16 @@ func (s *Service) commitFailedExport(ctx context.Context, scope coredata.Scoper,
)
}
func (s *Service) commitSuccessfulExport(ctx context.Context, scope coredata.Scoper, fe *coredata.FrameworkExport) error {
fe.CompletedAt = ref.Ref(time.Now())
fe.Status = coredata.FrameworkExportStatusCompleted
func (s *Service) commitSuccessfulExport(ctx context.Context, exportJob *coredata.ExportJob) error {
exportJob.CompletedAt = ref.Ref(time.Now())
exportJob.Status = coredata.ExportJobStatusCompleted
return s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
if err := fe.Update(ctx, conn, scope); err != nil {
return fmt.Errorf("cannot update framework export: %w", err)
scope := coredata.NewScope(exportJob.ID.TenantID())
if err := exportJob.Update(ctx, conn, scope); err != nil {
return fmt.Errorf("cannot update %s export job: %w", exportJob.Type, err)
}
return nil

View File

@@ -330,19 +330,19 @@ func (impl *Implm) Run(
}
}()
frameworkExporterCtx, stopFrameworkExporter := context.WithCancel(context.Background())
exportJobExporterCtx, stopExportJobExporter := context.WithCancel(context.Background())
wg.Add(1)
go func() {
defer wg.Done()
if err := impl.runFrameworkExporter(frameworkExporterCtx, proboService, l.Named("framework-exporter")); err != nil {
cancel(fmt.Errorf("framework exporter crashed: %w", err))
if err := impl.runExportJob(exportJobExporterCtx, proboService, l.Named("export-job-exporter")); err != nil {
cancel(fmt.Errorf("export job exporter crashed: %w", err))
}
}()
<-ctx.Done()
stopMailer()
stopFrameworkExporter()
stopExportJobExporter()
stopApiServer()
wg.Wait()
@@ -352,7 +352,7 @@ func (impl *Implm) Run(
return context.Cause(ctx)
}
func (impl *Implm) runFrameworkExporter(
func (impl *Implm) runExportJob(
ctx context.Context,
proboService *probo.Service,
l *log.Logger,
@@ -362,9 +362,9 @@ LOOP:
case <-ctx.Done():
return ctx.Err()
case <-time.After(30 * time.Second):
if err := proboService.ExportFrameworkJob(ctx); err != nil {
if !errors.Is(err, coredata.ErrNoFrameworkExportAvailable) {
l.ErrorCtx(ctx, "cannot process framework export", log.Error(err))
if err := proboService.ExportJob(ctx); err != nil {
if !errors.Is(err, coredata.ErrNoExportJobAvailable) {
l.ErrorCtx(ctx, "cannot process export job", log.Error(err))
}
}

View File

@@ -2643,6 +2643,12 @@ type Mutation {
bulkPublishDocumentVersions(
input: BulkPublishDocumentVersionsInput!
): BulkPublishDocumentVersionsPayload!
bulkDeleteDocuments(
input: BulkDeleteDocumentsInput!
): BulkDeleteDocumentsPayload!
bulkExportDocuments(
input: BulkExportDocumentsInput!
): BulkExportDocumentsPayload!
generateDocumentChangelog(
input: GenerateDocumentChangelogInput!
): GenerateDocumentChangelogPayload!
@@ -3856,6 +3862,22 @@ type BulkPublishDocumentVersionsPayload {
documentEdges: [DocumentEdge!]!
}
input BulkDeleteDocumentsInput {
documentIds: [ID!]!
}
input BulkExportDocumentsInput {
documentIds: [ID!]!
}
type BulkDeleteDocumentsPayload {
deletedDocumentIds: [ID!]!
}
type BulkExportDocumentsPayload {
exportJobId: ID!
}
input PublishDocumentVersionInput {
documentId: ID!
changelog: String

View File

@@ -159,6 +159,14 @@ type ComplexityRoot struct {
Node func(childComplexity int) int
}
BulkDeleteDocumentsPayload struct {
DeletedDocumentIds func(childComplexity int) int
}
BulkExportDocumentsPayload struct {
ExportJobID func(childComplexity int) int
}
BulkPublishDocumentVersionsPayload struct {
DocumentEdges func(childComplexity int) int
DocumentVersionEdges func(childComplexity int) int
@@ -714,6 +722,8 @@ type ComplexityRoot struct {
Mutation struct {
AssessVendor func(childComplexity int, input types.AssessVendorInput) int
AssignTask func(childComplexity int, input types.AssignTaskInput) int
BulkDeleteDocuments func(childComplexity int, input types.BulkDeleteDocumentsInput) int
BulkExportDocuments func(childComplexity int, input types.BulkExportDocumentsInput) int
BulkPublishDocumentVersions func(childComplexity int, input types.BulkPublishDocumentVersionsInput) int
BulkRequestSignatures func(childComplexity int, input types.BulkRequestSignaturesInput) int
CancelSignatureRequest func(childComplexity int, input types.CancelSignatureRequestInput) int
@@ -1629,6 +1639,8 @@ type MutationResolver interface {
DeleteDocument(ctx context.Context, input types.DeleteDocumentInput) (*types.DeleteDocumentPayload, error)
PublishDocumentVersion(ctx context.Context, input types.PublishDocumentVersionInput) (*types.PublishDocumentVersionPayload, error)
BulkPublishDocumentVersions(ctx context.Context, input types.BulkPublishDocumentVersionsInput) (*types.BulkPublishDocumentVersionsPayload, error)
BulkDeleteDocuments(ctx context.Context, input types.BulkDeleteDocumentsInput) (*types.BulkDeleteDocumentsPayload, error)
BulkExportDocuments(ctx context.Context, input types.BulkExportDocumentsInput) (*types.BulkExportDocumentsPayload, error)
GenerateDocumentChangelog(ctx context.Context, input types.GenerateDocumentChangelogInput) (*types.GenerateDocumentChangelogPayload, error)
CreateDraftDocumentVersion(ctx context.Context, input types.CreateDraftDocumentVersionInput) (*types.CreateDraftDocumentVersionPayload, error)
DeleteDraftDocumentVersion(ctx context.Context, input types.DeleteDraftDocumentVersionInput) (*types.DeleteDraftDocumentVersionPayload, error)
@@ -2090,6 +2102,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.AuditEdge.Node(childComplexity), true
case "BulkDeleteDocumentsPayload.deletedDocumentIds":
if e.complexity.BulkDeleteDocumentsPayload.DeletedDocumentIds == nil {
break
}
return e.complexity.BulkDeleteDocumentsPayload.DeletedDocumentIds(childComplexity), true
case "BulkExportDocumentsPayload.exportJobId":
if e.complexity.BulkExportDocumentsPayload.ExportJobID == nil {
break
}
return e.complexity.BulkExportDocumentsPayload.ExportJobID(childComplexity), true
case "BulkPublishDocumentVersionsPayload.documentEdges":
if e.complexity.BulkPublishDocumentVersionsPayload.DocumentEdges == nil {
break
@@ -3859,6 +3885,30 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Mutation.AssignTask(childComplexity, args["input"].(types.AssignTaskInput)), true
case "Mutation.bulkDeleteDocuments":
if e.complexity.Mutation.BulkDeleteDocuments == nil {
break
}
args, err := ec.field_Mutation_bulkDeleteDocuments_args(ctx, rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.BulkDeleteDocuments(childComplexity, args["input"].(types.BulkDeleteDocumentsInput)), true
case "Mutation.bulkExportDocuments":
if e.complexity.Mutation.BulkExportDocuments == nil {
break
}
args, err := ec.field_Mutation_bulkExportDocuments_args(ctx, rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.BulkExportDocuments(childComplexity, args["input"].(types.BulkExportDocumentsInput)), true
case "Mutation.bulkPublishDocumentVersions":
if e.complexity.Mutation.BulkPublishDocumentVersions == nil {
break
@@ -7834,6 +7884,8 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputAssetOrder,
ec.unmarshalInputAssignTaskInput,
ec.unmarshalInputAuditOrder,
ec.unmarshalInputBulkDeleteDocumentsInput,
ec.unmarshalInputBulkExportDocumentsInput,
ec.unmarshalInputBulkPublishDocumentVersionsInput,
ec.unmarshalInputBulkRequestSignaturesInput,
ec.unmarshalInputCancelSignatureRequestInput,
@@ -10724,6 +10776,12 @@ type Mutation {
bulkPublishDocumentVersions(
input: BulkPublishDocumentVersionsInput!
): BulkPublishDocumentVersionsPayload!
bulkDeleteDocuments(
input: BulkDeleteDocumentsInput!
): BulkDeleteDocumentsPayload!
bulkExportDocuments(
input: BulkExportDocumentsInput!
): BulkExportDocumentsPayload!
generateDocumentChangelog(
input: GenerateDocumentChangelogInput!
): GenerateDocumentChangelogPayload!
@@ -11937,6 +11995,22 @@ type BulkPublishDocumentVersionsPayload {
documentEdges: [DocumentEdge!]!
}
input BulkDeleteDocumentsInput {
documentIds: [ID!]!
}
input BulkExportDocumentsInput {
documentIds: [ID!]!
}
type BulkDeleteDocumentsPayload {
deletedDocumentIds: [ID!]!
}
type BulkExportDocumentsPayload {
exportJobId: ID!
}
input PublishDocumentVersionInput {
documentId: ID!
changelog: String
@@ -13861,6 +13935,52 @@ func (ec *executionContext) field_Mutation_assignTask_argsInput(
return zeroVal, nil
}
func (ec *executionContext) field_Mutation_bulkDeleteDocuments_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := ec.field_Mutation_bulkDeleteDocuments_argsInput(ctx, rawArgs)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_bulkDeleteDocuments_argsInput(
ctx context.Context,
rawArgs map[string]any,
) (types.BulkDeleteDocumentsInput, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
if tmp, ok := rawArgs["input"]; ok {
return ec.unmarshalNBulkDeleteDocumentsInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkDeleteDocumentsInput(ctx, tmp)
}
var zeroVal types.BulkDeleteDocumentsInput
return zeroVal, nil
}
func (ec *executionContext) field_Mutation_bulkExportDocuments_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
arg0, err := ec.field_Mutation_bulkExportDocuments_argsInput(ctx, rawArgs)
if err != nil {
return nil, err
}
args["input"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_bulkExportDocuments_argsInput(
ctx context.Context,
rawArgs map[string]any,
) (types.BulkExportDocumentsInput, error) {
ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
if tmp, ok := rawArgs["input"]; ok {
return ec.unmarshalNBulkExportDocumentsInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkExportDocumentsInput(ctx, tmp)
}
var zeroVal types.BulkExportDocumentsInput
return zeroVal, nil
}
func (ec *executionContext) field_Mutation_bulkPublishDocumentVersions_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -21578,6 +21698,94 @@ func (ec *executionContext) fieldContext_AuditEdge_node(_ context.Context, field
return fc, nil
}
func (ec *executionContext) _BulkDeleteDocumentsPayload_deletedDocumentIds(ctx context.Context, field graphql.CollectedField, obj *types.BulkDeleteDocumentsPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_BulkDeleteDocumentsPayload_deletedDocumentIds(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.DeletedDocumentIds, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.([]gid.GID)
fc.Result = res
return ec.marshalNID2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGIDᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_BulkDeleteDocumentsPayload_deletedDocumentIds(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "BulkDeleteDocumentsPayload",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type ID does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _BulkExportDocumentsPayload_exportJobId(ctx context.Context, field graphql.CollectedField, obj *types.BulkExportDocumentsPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_BulkExportDocumentsPayload_exportJobId(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return obj.ExportJobID, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(gid.GID)
fc.Result = res
return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_BulkExportDocumentsPayload_exportJobId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "BulkExportDocumentsPayload",
Field: field,
IsMethod: false,
IsResolver: false,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
return nil, errors.New("field of type ID does not have child fields")
},
}
return fc, nil
}
func (ec *executionContext) _BulkPublishDocumentVersionsPayload_documentVersionEdges(ctx context.Context, field graphql.CollectedField, obj *types.BulkPublishDocumentVersionsPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_BulkPublishDocumentVersionsPayload_documentVersionEdges(ctx, field)
if err != nil {
@@ -37958,6 +38166,124 @@ func (ec *executionContext) fieldContext_Mutation_bulkPublishDocumentVersions(ct
return fc, nil
}
func (ec *executionContext) _Mutation_bulkDeleteDocuments(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_bulkDeleteDocuments(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().BulkDeleteDocuments(rctx, fc.Args["input"].(types.BulkDeleteDocumentsInput))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*types.BulkDeleteDocumentsPayload)
fc.Result = res
return ec.marshalNBulkDeleteDocumentsPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkDeleteDocumentsPayload(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Mutation_bulkDeleteDocuments(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "deletedDocumentIds":
return ec.fieldContext_BulkDeleteDocumentsPayload_deletedDocumentIds(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type BulkDeleteDocumentsPayload", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_bulkDeleteDocuments_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Mutation_bulkExportDocuments(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_bulkExportDocuments(ctx, field)
if err != nil {
return graphql.Null
}
ctx = graphql.WithFieldContext(ctx, fc)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().BulkExportDocuments(rctx, fc.Args["input"].(types.BulkExportDocumentsInput))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*types.BulkExportDocumentsPayload)
fc.Result = res
return ec.marshalNBulkExportDocumentsPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkExportDocumentsPayload(ctx, field.Selections, res)
}
func (ec *executionContext) fieldContext_Mutation_bulkExportDocuments(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
fc = &graphql.FieldContext{
Object: "Mutation",
Field: field,
IsMethod: true,
IsResolver: true,
Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
switch field.Name {
case "exportJobId":
return ec.fieldContext_BulkExportDocumentsPayload_exportJobId(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type BulkExportDocumentsPayload", field.Name)
},
}
defer func() {
if r := recover(); r != nil {
err = ec.Recover(ctx, r)
ec.Error(ctx, err)
}
}()
ctx = graphql.WithFieldContext(ctx, fc)
if fc.Args, err = ec.field_Mutation_bulkExportDocuments_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
ec.Error(ctx, err)
return fc, err
}
return fc, nil
}
func (ec *executionContext) _Mutation_generateDocumentChangelog(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Mutation_generateDocumentChangelog(ctx, field)
if err != nil {
@@ -61021,6 +61347,60 @@ func (ec *executionContext) unmarshalInputAuditOrder(ctx context.Context, obj an
return it, nil
}
func (ec *executionContext) unmarshalInputBulkDeleteDocumentsInput(ctx context.Context, obj any) (types.BulkDeleteDocumentsInput, error) {
var it types.BulkDeleteDocumentsInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"documentIds"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "documentIds":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("documentIds"))
data, err := ec.unmarshalNID2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGIDᚄ(ctx, v)
if err != nil {
return it, err
}
it.DocumentIds = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputBulkExportDocumentsInput(ctx context.Context, obj any) (types.BulkExportDocumentsInput, error) {
var it types.BulkExportDocumentsInput
asMap := map[string]any{}
for k, v := range obj.(map[string]any) {
asMap[k] = v
}
fieldsInOrder := [...]string{"documentIds"}
for _, k := range fieldsInOrder {
v, ok := asMap[k]
if !ok {
continue
}
switch k {
case "documentIds":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("documentIds"))
data, err := ec.unmarshalNID2ᚕgithubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGIDᚄ(ctx, v)
if err != nil {
return it, err
}
it.DocumentIds = data
}
}
return it, nil
}
func (ec *executionContext) unmarshalInputBulkPublishDocumentVersionsInput(ctx context.Context, obj any) (types.BulkPublishDocumentVersionsInput, error) {
var it types.BulkPublishDocumentVersionsInput
asMap := map[string]any{}
@@ -68443,6 +68823,84 @@ func (ec *executionContext) _AuditEdge(ctx context.Context, sel ast.SelectionSet
return out
}
var bulkDeleteDocumentsPayloadImplementors = []string{"BulkDeleteDocumentsPayload"}
func (ec *executionContext) _BulkDeleteDocumentsPayload(ctx context.Context, sel ast.SelectionSet, obj *types.BulkDeleteDocumentsPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, bulkDeleteDocumentsPayloadImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("BulkDeleteDocumentsPayload")
case "deletedDocumentIds":
out.Values[i] = ec._BulkDeleteDocumentsPayload_deletedDocumentIds(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var bulkExportDocumentsPayloadImplementors = []string{"BulkExportDocumentsPayload"}
func (ec *executionContext) _BulkExportDocumentsPayload(ctx context.Context, sel ast.SelectionSet, obj *types.BulkExportDocumentsPayload) graphql.Marshaler {
fields := graphql.CollectFields(ec.OperationContext, sel, bulkExportDocumentsPayloadImplementors)
out := graphql.NewFieldSet(fields)
deferred := make(map[string]*graphql.FieldSet)
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("BulkExportDocumentsPayload")
case "exportJobId":
out.Values[i] = ec._BulkExportDocumentsPayload_exportJobId(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch(ctx)
if out.Invalids > 0 {
return graphql.Null
}
atomic.AddInt32(&ec.deferred, int32(len(deferred)))
for label, dfs := range deferred {
ec.processDeferredGroup(graphql.DeferredGroup{
Label: label,
Path: graphql.GetPath(ctx),
FieldSet: dfs,
Context: ctx,
})
}
return out
}
var bulkPublishDocumentVersionsPayloadImplementors = []string{"BulkPublishDocumentVersionsPayload"}
func (ec *executionContext) _BulkPublishDocumentVersionsPayload(ctx context.Context, sel ast.SelectionSet, obj *types.BulkPublishDocumentVersionsPayload) graphql.Marshaler {
@@ -74828,6 +75286,20 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "bulkDeleteDocuments":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_bulkDeleteDocuments(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "bulkExportDocuments":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_bulkExportDocuments(ctx, field)
})
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "generateDocumentChangelog":
out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
return ec._Mutation_generateDocumentChangelog(ctx, field)
@@ -82656,6 +83128,44 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se
return res
}
func (ec *executionContext) unmarshalNBulkDeleteDocumentsInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkDeleteDocumentsInput(ctx context.Context, v any) (types.BulkDeleteDocumentsInput, error) {
res, err := ec.unmarshalInputBulkDeleteDocumentsInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNBulkDeleteDocumentsPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkDeleteDocumentsPayload(ctx context.Context, sel ast.SelectionSet, v types.BulkDeleteDocumentsPayload) graphql.Marshaler {
return ec._BulkDeleteDocumentsPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNBulkDeleteDocumentsPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkDeleteDocumentsPayload(ctx context.Context, sel ast.SelectionSet, v *types.BulkDeleteDocumentsPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._BulkDeleteDocumentsPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNBulkExportDocumentsInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkExportDocumentsInput(ctx context.Context, v any) (types.BulkExportDocumentsInput, error) {
res, err := ec.unmarshalInputBulkExportDocumentsInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalNBulkExportDocumentsPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkExportDocumentsPayload(ctx context.Context, sel ast.SelectionSet, v types.BulkExportDocumentsPayload) graphql.Marshaler {
return ec._BulkExportDocumentsPayload(ctx, sel, &v)
}
func (ec *executionContext) marshalNBulkExportDocumentsPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkExportDocumentsPayload(ctx context.Context, sel ast.SelectionSet, v *types.BulkExportDocumentsPayload) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "the requested element is null which the schema does not allow")
}
return graphql.Null
}
return ec._BulkExportDocumentsPayload(ctx, sel, v)
}
func (ec *executionContext) unmarshalNBulkPublishDocumentVersionsInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐBulkPublishDocumentVersionsInput(ctx context.Context, v any) (types.BulkPublishDocumentVersionsInput, error) {
res, err := ec.unmarshalInputBulkPublishDocumentVersionsInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)

View File

@@ -85,6 +85,22 @@ type AuditEdge struct {
Node *Audit `json:"node"`
}
type BulkDeleteDocumentsInput struct {
DocumentIds []gid.GID `json:"documentIds"`
}
type BulkDeleteDocumentsPayload struct {
DeletedDocumentIds []gid.GID `json:"deletedDocumentIds"`
}
type BulkExportDocumentsInput struct {
DocumentIds []gid.GID `json:"documentIds"`
}
type BulkExportDocumentsPayload struct {
ExportJobID gid.GID `json:"exportJobId"`
}
type BulkPublishDocumentVersionsInput struct {
DocumentIds []gid.GID `json:"documentIds"`
Changelog string `json:"changelog"`

View File

@@ -2543,6 +2543,49 @@ func (r *mutationResolver) BulkPublishDocumentVersions(ctx context.Context, inpu
}, nil
}
// BulkDeleteDocuments is the resolver for the bulkDeleteDocuments field.
func (r *mutationResolver) BulkDeleteDocuments(ctx context.Context, input types.BulkDeleteDocumentsInput) (*types.BulkDeleteDocumentsPayload, error) {
if len(input.DocumentIds) == 0 {
return &types.BulkDeleteDocumentsPayload{
DeletedDocumentIds: []gid.GID{},
}, nil
}
prb := r.ProboService(ctx, input.DocumentIds[0].TenantID())
err := prb.Documents.BulkSoftDelete(ctx, input.DocumentIds)
if err != nil {
panic(fmt.Errorf("cannot bulk delete documents: %w", err))
}
return &types.BulkDeleteDocumentsPayload{
DeletedDocumentIds: input.DocumentIds,
}, nil
}
// BulkExportDocuments is the resolver for the bulkExportDocuments field.
func (r *mutationResolver) BulkExportDocuments(ctx context.Context, input types.BulkExportDocumentsInput) (*types.BulkExportDocumentsPayload, error) {
if len(input.DocumentIds) == 0 {
panic(fmt.Errorf("no document ids provided"))
}
prb := r.ProboService(ctx, input.DocumentIds[0].TenantID())
user := UserFromContext(ctx)
if user == nil {
panic(fmt.Errorf("user not found"))
}
documentExport, err := prb.Documents.RequestExport(ctx, input.DocumentIds, user.EmailAddress, user.FullName)
if err != nil {
panic(fmt.Errorf("cannot request document export: %w", err))
}
return &types.BulkExportDocumentsPayload{
ExportJobID: documentExport.ID,
}, nil
}
// GenerateDocumentChangelog is the resolver for the generateDocumentChangelog field.
func (r *mutationResolver) GenerateDocumentChangelog(ctx context.Context, input types.GenerateDocumentChangelogInput) (*types.GenerateDocumentChangelogPayload, error) {
prb := r.ProboService(ctx, input.DocumentID.TenantID())