github.com/decred/politeia@v1.4.0/politeiawww/legacy/cmsdatabase/cockroachdb/cockroachdb.go (about)

     1  // Copyright (c) 2017-2020 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package cockroachdb
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"net/url"
    11  	"path/filepath"
    12  	"sync"
    13  	"time"
    14  
    15  	v1 "github.com/decred/politeia/politeiawww/api/cms/v1"
    16  	database "github.com/decred/politeia/politeiawww/legacy/cmsdatabase"
    17  	"github.com/jinzhu/gorm"
    18  	_ "github.com/jinzhu/gorm/dialects/postgres"
    19  )
    20  
    21  const (
    22  	cacheID    = "cms"
    23  	cmsVersion = "2"
    24  
    25  	// Database table names
    26  	tableNameVersions      = "versions"
    27  	tableNameInvoice       = "invoices"
    28  	tableNameLineItem      = "line_items"
    29  	tableNameInvoiceChange = "invoice_changes"
    30  	tableNameExchangeRate  = "exchange_rates"
    31  	tableNamePayments      = "payments"
    32  	tableNameDCC           = "dcc"
    33  
    34  	userPoliteiawww = "politeiawww" // cmsdb user (read/write access)
    35  )
    36  
    37  // cockroachdb implements the cache interface.
    38  type cockroachdb struct {
    39  	sync.RWMutex
    40  	shutdown  bool     // Backend is shutdown
    41  	recordsdb *gorm.DB // Database context
    42  }
    43  
    44  // Create new invoice.
    45  //
    46  // CreateInvoice satisfies the database interface.
    47  func (c *cockroachdb) NewInvoice(dbInvoice *database.Invoice) error {
    48  	invoice := EncodeInvoice(dbInvoice)
    49  
    50  	log.Debugf("CreateInvoice: %v", invoice.Token)
    51  	return c.recordsdb.Create(invoice).Error
    52  }
    53  
    54  // Update existing invoice.
    55  //
    56  // UpdateInvoice satisfies the database interface.
    57  func (c *cockroachdb) UpdateInvoice(dbInvoice *database.Invoice) error {
    58  	invoice := EncodeInvoice(dbInvoice)
    59  
    60  	log.Debugf("UpdateInvoice: %v", invoice.Token)
    61  
    62  	return c.recordsdb.Save(invoice).Error
    63  }
    64  
    65  // RemoveLineItem deletes an existing invoice line items from the database.
    66  func (c *cockroachdb) RemoveInvoiceLineItems(invoiceToken string) error {
    67  	log.Debugf("RemoveInvoiceLineItems: %v", invoiceToken)
    68  
    69  	return c.recordsdb.Where("invoice_token = ?", invoiceToken).Delete(&LineItem{}).Error
    70  }
    71  
    72  // Return all invoices by userid
    73  func (c *cockroachdb) InvoicesByUserID(userid string) ([]database.Invoice, error) {
    74  	log.Tracef("InvoicesByUserID")
    75  
    76  	// Lookup the latest version of each invoice
    77  	query := `SELECT *
    78              FROM invoices a
    79              LEFT OUTER JOIN invoices b
    80                ON a.token = b.token
    81                AND a.version < b.version
    82                WHERE b.token IS NULL
    83                AND a.user_id = ?`
    84  	rows, err := c.recordsdb.Raw(query, userid).Rows()
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	defer rows.Close()
    89  
    90  	invoices := make([]Invoice, 0, 1024) // PNOOMA
    91  	for rows.Next() {
    92  		var i Invoice
    93  		err := c.recordsdb.ScanRows(rows, &i)
    94  		if err != nil {
    95  			return nil, err
    96  		}
    97  		invoices = append(invoices, i)
    98  	}
    99  	if err = rows.Err(); err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	keys := make([]string, 0, len(invoices))
   104  	for _, r := range invoices {
   105  		keys = append(keys, r.Key)
   106  	}
   107  	err = c.recordsdb.
   108  		Preload("LineItems").
   109  		Preload("Changes").
   110  		Preload("Payments").
   111  		Where(keys).
   112  		Find(&invoices).
   113  		Error
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	dbInvoices := make([]database.Invoice, 0, len(invoices))
   119  	for _, v := range invoices {
   120  		dbInvoice, err := DecodeInvoice(&v)
   121  		if err != nil {
   122  			return nil, err
   123  		}
   124  		dbInvoices = append(dbInvoices, *dbInvoice)
   125  	}
   126  	return dbInvoices, nil
   127  }
   128  
   129  // InvoiceByToken Return invoice by its token.
   130  func (c *cockroachdb) InvoiceByToken(token string) (*database.Invoice, error) {
   131  	log.Debugf("InvoiceByToken: %v", token)
   132  
   133  	invoice := Invoice{}
   134  	err := c.recordsdb.
   135  		Where("token = ?", token).
   136  		Order("version desc").
   137  		Limit(1).
   138  		Preload("LineItems").
   139  		Preload("Changes").
   140  		Preload("Payments").
   141  		Find(&invoice).Error
   142  	if err != nil {
   143  		if errors.Is(err, gorm.ErrRecordNotFound) {
   144  			err = database.ErrInvoiceNotFound
   145  		}
   146  		return nil, err
   147  	}
   148  
   149  	return DecodeInvoice(&invoice)
   150  }
   151  
   152  // InvoiceByKey Return invoice by its key.
   153  func (c *cockroachdb) InvoiceByKey(key string) (*database.Invoice, error) {
   154  	log.Debugf("InvoiceByKey: %v", key)
   155  
   156  	invoice := Invoice{}
   157  	err := c.recordsdb.
   158  		Where("key = ?", key).
   159  		Order("version desc").
   160  		Limit(1).
   161  		Preload("LineItems").
   162  		Preload("Changes").
   163  		Preload("Payments").
   164  		Find(&invoice).Error
   165  	if err != nil {
   166  		if errors.Is(err, gorm.ErrRecordNotFound) {
   167  			err = database.ErrInvoiceNotFound
   168  		}
   169  		return nil, err
   170  	}
   171  
   172  	return DecodeInvoice(&invoice)
   173  }
   174  
   175  // InvoiceByTokenVersion Return invoice by its token and version
   176  func (c *cockroachdb) InvoiceByTokenVersion(token string, version string) (*database.Invoice, error) {
   177  	log.Debugf("InvoiceByTokenVersion: %v", token)
   178  
   179  	invoice := Invoice{}
   180  	err := c.recordsdb.
   181  		Where("key = ?", token+version).
   182  		Preload("LineItems").
   183  		Preload("Changes").
   184  		Preload("Payments").
   185  		Find(&invoice).Error
   186  	if err != nil {
   187  		if errors.Is(err, gorm.ErrRecordNotFound) {
   188  			err = database.ErrInvoiceNotFound
   189  		}
   190  		return nil, err
   191  	}
   192  
   193  	return DecodeInvoice(&invoice)
   194  }
   195  
   196  // InvoicesByMonthYearStatus returns all invoices by month year and status
   197  func (c *cockroachdb) InvoicesByMonthYearStatus(month, year uint16, status int) ([]database.Invoice, error) {
   198  	log.Tracef("InvoicesByMonthYearStatus")
   199  
   200  	// Lookup the latest version of each invoice
   201  	query := `SELECT *
   202              FROM invoices a
   203              LEFT OUTER JOIN invoices b
   204                ON a.token = b.token
   205                AND a.version < b.version
   206                WHERE b.token IS NULL
   207                AND month = ? AND year = ? AND status = ?`
   208  	rows, err := c.recordsdb.Raw(query, month, year, status).Rows()
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	defer rows.Close()
   213  
   214  	invoices := make([]Invoice, 0, 1024) // PNOOMA
   215  	for rows.Next() {
   216  		var i Invoice
   217  		err := c.recordsdb.ScanRows(rows, &i)
   218  		if err != nil {
   219  			return nil, err
   220  		}
   221  		invoices = append(invoices, i)
   222  	}
   223  	if err = rows.Err(); err != nil {
   224  		return nil, err
   225  	}
   226  	keys := make([]string, 0, len(invoices))
   227  	for _, r := range invoices {
   228  		keys = append(keys, r.Key)
   229  	}
   230  	err = c.recordsdb.
   231  		Preload("LineItems").
   232  		Preload("Changes").
   233  		Preload("Payments").
   234  		Where(keys).
   235  		Find(&invoices).
   236  		Error
   237  	if err != nil {
   238  		return nil, err
   239  	}
   240  
   241  	dbInvoices := make([]database.Invoice, 0, len(invoices))
   242  	for _, v := range invoices {
   243  		dbInvoice, err := DecodeInvoice(&v)
   244  		if err != nil {
   245  			return nil, err
   246  		}
   247  		dbInvoices = append(dbInvoices, *dbInvoice)
   248  	}
   249  	return dbInvoices, nil
   250  }
   251  
   252  // InvoicesByMonthYear returns all invoices by month/year
   253  func (c *cockroachdb) InvoicesByMonthYear(month, year uint16) ([]database.Invoice, error) {
   254  	log.Tracef("InvoicesByMonthYear")
   255  
   256  	// Lookup the latest version of each invoice
   257  	query := `SELECT *
   258              FROM invoices a
   259              LEFT OUTER JOIN invoices b
   260                ON a.token = b.token
   261                AND a.version < b.version
   262                WHERE b.token IS NULL
   263                AND a.month = ? AND a.year = ?`
   264  	rows, err := c.recordsdb.Raw(query, month, year).Rows()
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  	defer rows.Close()
   269  
   270  	invoices := make([]Invoice, 0, 1024) // PNOOMA
   271  	for rows.Next() {
   272  		var i Invoice
   273  		err := c.recordsdb.ScanRows(rows, &i)
   274  		if err != nil {
   275  			return nil, err
   276  		}
   277  		invoices = append(invoices, i)
   278  	}
   279  	if err = rows.Err(); err != nil {
   280  		return nil, err
   281  	}
   282  
   283  	keys := make([]string, 0, len(invoices))
   284  	for _, r := range invoices {
   285  		keys = append(keys, r.Key)
   286  	}
   287  	err = c.recordsdb.
   288  		Preload("LineItems").
   289  		Preload("Changes").
   290  		Preload("Payments").
   291  		Where(keys).
   292  		Find(&invoices).
   293  		Error
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	dbInvoices := make([]database.Invoice, 0, len(invoices))
   299  	for _, v := range invoices {
   300  		dbInvoice, err := DecodeInvoice(&v)
   301  		if err != nil {
   302  			return nil, err
   303  		}
   304  		dbInvoices = append(dbInvoices, *dbInvoice)
   305  	}
   306  	return dbInvoices, nil
   307  }
   308  
   309  // InvoicesByStatus returns all invoices by status
   310  func (c *cockroachdb) InvoicesByStatus(status int) ([]database.Invoice, error) {
   311  	log.Tracef("InvoicesByStatus")
   312  
   313  	// Lookup the latest version of each invoice
   314  	query := `SELECT *
   315              FROM invoices a
   316              LEFT OUTER JOIN invoices b
   317                ON a.token = b.token
   318                AND a.version < b.version
   319                WHERE a.status = ?`
   320  	rows, err := c.recordsdb.Raw(query, status).Rows()
   321  	if err != nil {
   322  		return nil, err
   323  	}
   324  	defer rows.Close()
   325  
   326  	invoices := make([]Invoice, 0, 1024) // PNOOMA
   327  	for rows.Next() {
   328  		var i Invoice
   329  		err := c.recordsdb.ScanRows(rows, &i)
   330  		if err != nil {
   331  			return nil, err
   332  		}
   333  		invoices = append(invoices, i)
   334  	}
   335  	if err = rows.Err(); err != nil {
   336  		return nil, err
   337  	}
   338  
   339  	// Compile a list of record primary keys
   340  	keys := make([]string, 0, len(invoices))
   341  	for _, v := range invoices {
   342  		keys = append(keys, v.Key)
   343  	}
   344  
   345  	// Lookup files and metadata streams for each of the
   346  	// previously queried records.
   347  	err = c.recordsdb.
   348  		Preload("LineItems").
   349  		Preload("Changes").
   350  		Preload("Payments").
   351  		Where(keys).
   352  		Find(&invoices).
   353  		Error
   354  	if err != nil {
   355  		return nil, err
   356  	}
   357  	dbInvoices := make([]database.Invoice, 0, len(invoices))
   358  	for _, v := range invoices {
   359  		dbInvoice, err := DecodeInvoice(&v)
   360  		if err != nil {
   361  			return nil, err
   362  		}
   363  		dbInvoices = append(dbInvoices, *dbInvoice)
   364  	}
   365  	return dbInvoices, nil
   366  }
   367  
   368  // InvoicesAll returns all invoices
   369  func (c *cockroachdb) InvoicesAll() ([]database.Invoice, error) {
   370  	// Lookup the latest version of each invoice
   371  	query := `SELECT *
   372              FROM invoices a
   373              LEFT OUTER JOIN invoices b
   374                ON a.token = b.token
   375                AND a.version < b.version`
   376  	rows, err := c.recordsdb.Raw(query).Rows()
   377  	if err != nil {
   378  		return nil, err
   379  	}
   380  	defer rows.Close()
   381  
   382  	invoices := make([]Invoice, 0, 1024) // PNOOMA
   383  	for rows.Next() {
   384  		var i Invoice
   385  		err := c.recordsdb.ScanRows(rows, &i)
   386  		if err != nil {
   387  			return nil, err
   388  		}
   389  		invoices = append(invoices, i)
   390  	}
   391  	if err = rows.Err(); err != nil {
   392  		return nil, err
   393  	}
   394  
   395  	// Compile a list of record primary keys
   396  	keys := make([]string, 0, len(invoices))
   397  	for _, v := range invoices {
   398  		keys = append(keys, v.Key)
   399  	}
   400  
   401  	// Lookup files and metadata streams for each of the
   402  	// previously queried records.
   403  	err = c.recordsdb.
   404  		Preload("LineItems").
   405  		Preload("Changes").
   406  		Preload("Payments").
   407  		Where(keys).
   408  		Find(&invoices).
   409  		Error
   410  	if err != nil {
   411  		return nil, err
   412  	}
   413  
   414  	dbInvoices := make([]database.Invoice, 0, len(invoices))
   415  	for _, v := range invoices {
   416  		dbInvoice, err := DecodeInvoice(&v)
   417  		if err != nil {
   418  			return nil, err
   419  		}
   420  		dbInvoices = append(dbInvoices, *dbInvoice)
   421  	}
   422  	return dbInvoices, nil
   423  }
   424  
   425  // InvoicesByAddress return invoices by its payment address.
   426  func (c *cockroachdb) InvoicesByAddress(address string) ([]database.Invoice, error) {
   427  	log.Debugf("InvoiceByAddress: %v", address)
   428  
   429  	// Lookup the latest version of each invoice
   430  	query := `SELECT *
   431              FROM invoices a
   432              LEFT OUTER JOIN invoices b
   433                ON a.token = b.token
   434                AND a.version < b.version
   435                WHERE b.token IS NULL
   436                AND a.payment_address = ?`
   437  	rows, err := c.recordsdb.Raw(query, address).Rows()
   438  	if err != nil {
   439  		return nil, err
   440  	}
   441  	defer rows.Close()
   442  
   443  	invoices := make([]Invoice, 0, 1024) // PNOOMA
   444  	for rows.Next() {
   445  		var i Invoice
   446  		err := c.recordsdb.ScanRows(rows, &i)
   447  		if err != nil {
   448  			return nil, err
   449  		}
   450  		invoices = append(invoices, i)
   451  	}
   452  	if err = rows.Err(); err != nil {
   453  		return nil, err
   454  	}
   455  
   456  	// Compile a list of record primary keys
   457  	keys := make([]string, 0, len(invoices))
   458  	for _, v := range invoices {
   459  		keys = append(keys, v.Key)
   460  	}
   461  
   462  	// Lookup files and metadata streams for each of the
   463  	// previously queried records.
   464  	err = c.recordsdb.
   465  		Preload("LineItems").
   466  		Preload("Changes").
   467  		Preload("Payments").
   468  		Where(keys).
   469  		Find(&invoices).
   470  		Error
   471  	if err != nil {
   472  		return nil, err
   473  	}
   474  
   475  	dbInvoices := make([]database.Invoice, 0, len(invoices))
   476  	for _, v := range invoices {
   477  		dbInvoice, err := DecodeInvoice(&v)
   478  		if err != nil {
   479  			return nil, err
   480  		}
   481  		dbInvoices = append(dbInvoices, *dbInvoice)
   482  	}
   483  
   484  	return dbInvoices, nil
   485  }
   486  
   487  // Create new exchange rate.
   488  //
   489  // NewExchangeRate satisfies the database interface.
   490  func (c *cockroachdb) NewExchangeRate(dbExchangeRate *database.ExchangeRate) error {
   491  	exchRate := encodeExchangeRate(dbExchangeRate)
   492  
   493  	log.Debugf("NewExchangeRate: %v %v", exchRate.Month, exchRate.Year)
   494  	return c.recordsdb.Create(exchRate).Error
   495  }
   496  
   497  // ExchangeRate returns exchange rate by month/year
   498  func (c *cockroachdb) ExchangeRate(month, year int) (*database.ExchangeRate, error) {
   499  	log.Tracef("ExchangeRate")
   500  
   501  	exchangeRate := ExchangeRate{}
   502  	err := c.recordsdb.
   503  		Where("month = ? AND year = ?", month, year).
   504  		Find(&exchangeRate).
   505  		Error
   506  	if err != nil {
   507  		if errors.Is(err, gorm.ErrRecordNotFound) {
   508  			err = database.ErrExchangeRateNotFound
   509  		}
   510  		return nil, err
   511  	}
   512  
   513  	return decodeExchangeRate(exchangeRate), nil
   514  }
   515  
   516  // InvoicesByDateRangeStatus takes a start and end time (in Unix seconds) and returns
   517  // all invoices with the included status.  This uses the
   518  // invoice_changes table to discover the token to look up the correct line items.
   519  func (c *cockroachdb) InvoicesByDateRangeStatus(start, end int64, status int) ([]database.Invoice, error) {
   520  	log.Debugf("InvoicesByDateRangeStatus: %v %v", time.Unix(start, 0),
   521  		time.Unix(end, 0))
   522  	// Get all invoice changes of given status within date range.
   523  	invoiceChanges := make([]InvoiceChange, 0, 1024) // PNOOMA
   524  	err := c.recordsdb.
   525  		Where("new_status = ? AND "+
   526  			"timestamp BETWEEN ? AND ?",
   527  			status,
   528  			time.Unix(start, 0),
   529  			time.Unix(end, 0)).
   530  		Find(&invoiceChanges).
   531  		Error
   532  	if err != nil {
   533  		return nil, err
   534  	}
   535  	tokens := make([]string, 0, len(invoiceChanges))
   536  	for _, r := range invoiceChanges {
   537  		tokens = append(tokens, r.InvoiceToken)
   538  	}
   539  	invoices := make([]Invoice, 0, len(tokens))
   540  	// Using all invoice tokens from the results of the query above, ask for all
   541  	// invoices that match those tokens.
   542  	dbInvoices := make([]database.Invoice, 0, len(tokens))
   543  	err = c.recordsdb.
   544  		Preload("LineItems").
   545  		Preload("Payments").
   546  		Preload("Changes").
   547  		Where(tokens).
   548  		Find(&invoices).
   549  		Error
   550  	if err != nil {
   551  		return nil, err
   552  	}
   553  	for _, vv := range invoices {
   554  		inv, err := DecodeInvoice(&vv)
   555  		if err != nil {
   556  			return nil, err
   557  		}
   558  		dbInvoices = append(dbInvoices, *inv)
   559  	}
   560  	return dbInvoices, nil
   561  }
   562  
   563  // InvoicesByDateRange takes a start and end time (in Unix seconds) and returns
   564  // all invoices.  This uses the
   565  // invoice_changes table to discover the token to look up the correct line items.
   566  func (c *cockroachdb) InvoicesByDateRange(start, end int64) ([]database.Invoice, error) {
   567  	log.Debugf("InvoicesByDateRange: %v %v", time.Unix(start, 0),
   568  		time.Unix(end, 0))
   569  	// Get all invoice changes of NEW status within date range.
   570  	invoiceChanges := make([]InvoiceChange, 0, 1024) // PNOOMA
   571  	err := c.recordsdb.
   572  		Where("new_status = ? AND"+
   573  			"timestamp BETWEEN ? AND ?",
   574  			v1.InvoiceStatusNew,
   575  			time.Unix(start, 0),
   576  			time.Unix(end, 0)).
   577  		Find(&invoiceChanges).
   578  		Error
   579  	if err != nil {
   580  		return nil, err
   581  	}
   582  	tokens := make([]string, 0, len(invoiceChanges))
   583  	for _, r := range invoiceChanges {
   584  		tokens = append(tokens, r.InvoiceToken)
   585  	}
   586  	invoices := make([]Invoice, 0, len(tokens))
   587  	// Using all invoice tokens from the results of the query above, ask for all
   588  	// invoices that match those tokens.
   589  	dbInvoices := make([]database.Invoice, 0, len(tokens))
   590  	err = c.recordsdb.
   591  		Preload("LineItems").
   592  		Preload("Changes").
   593  		Preload("Payments").
   594  		Where(tokens).
   595  		Find(&invoices).
   596  		Error
   597  	if err != nil {
   598  		return nil, err
   599  	}
   600  	for _, vv := range invoices {
   601  		inv, err := DecodeInvoice(&vv)
   602  		if err != nil {
   603  			return nil, err
   604  		}
   605  		dbInvoices = append(dbInvoices, *inv)
   606  	}
   607  	return dbInvoices, nil
   608  }
   609  
   610  // InvoicesByLineItemsProposalToken takes a proposal token as an argument and
   611  // returns all invoices that have line items corresponding with that token.
   612  // All line items that are not considered relevant to the proposal token will
   613  // be omitted.
   614  func (c *cockroachdb) InvoicesByLineItemsProposalToken(token string) ([]database.Invoice, error) {
   615  	log.Debugf("InvoicesByLineItemsProposalToken: %v", token)
   616  	// Get all line items from approved, new, updated or paid invoices based
   617  	// on the provided proposal token
   618  	query := `SELECT 
   619                invoices.month, 
   620                invoices.year, 
   621                invoices.user_id,
   622                invoices.public_key,
   623                invoices.contractor_rate,
   624                invoices.exchange_rate,
   625                line_items.invoice_token,
   626                line_items.type,
   627                line_items.domain,
   628                line_items.subdomain,
   629                line_items.description,
   630                line_items.proposal_url,
   631                line_items.labor,
   632                line_items.expenses,
   633                line_items.contractor_rate AS sub_rate,
   634                line_items.sub_user_id as sub_user
   635              FROM invoices
   636              LEFT OUTER JOIN invoices b
   637                ON invoices.token = b.token
   638                AND invoices.version < b.version
   639              INNER JOIN line_items
   640                ON invoices.key = line_items.invoice_key
   641                WHERE line_items.proposal_url = ? AND (
   642  				invoices.status = ? OR 
   643  				invoices.status = ? OR 
   644  				invoices.status = ? OR
   645  				invoices.status = ?)`
   646  	rows, err := c.recordsdb.Raw(query, token,
   647  		int(v1.InvoiceStatusNew),
   648  		int(v1.InvoiceStatusUpdated),
   649  		int(v1.InvoiceStatusApproved),
   650  		int(v1.InvoiceStatusPaid),
   651  	).Rows()
   652  	if err != nil {
   653  		return nil, err
   654  	}
   655  	defer rows.Close()
   656  
   657  	matching := make([]MatchingLineItems, 0, 1024)
   658  	for rows.Next() {
   659  		var i MatchingLineItems
   660  		err := c.recordsdb.ScanRows(rows, &i)
   661  		if err != nil {
   662  			return nil, err
   663  		}
   664  		matching = append(matching, i)
   665  	}
   666  	if err = rows.Err(); err != nil {
   667  		return nil, err
   668  	}
   669  	return convertMatchingLineItemToInvoices(matching), nil
   670  }
   671  
   672  // MatchingLineItems is a type used for finding matched line items based on
   673  // proposal ownership.
   674  type MatchingLineItems struct {
   675  	InvoiceToken   string
   676  	UserID         string
   677  	Month          uint
   678  	Year           uint
   679  	Type           uint
   680  	Domain         string
   681  	Subdomain      string
   682  	Description    string
   683  	ProposalURL    string
   684  	Labor          uint
   685  	Expenses       uint
   686  	ContractorRate uint
   687  	PublicKey      string
   688  	ExchangeRate   uint
   689  	SubRate        uint
   690  	SubUser        string
   691  }
   692  
   693  // Close satisfies the database interface.
   694  func (c *cockroachdb) Close() error {
   695  	return c.recordsdb.Close()
   696  }
   697  
   698  // This function must be called within a transaction.
   699  func createCmsTables(tx *gorm.DB) error {
   700  	log.Tracef("createCmsTables")
   701  
   702  	// Create cms tables
   703  	if !tx.HasTable(tableNameInvoice) {
   704  		err := tx.CreateTable(&Invoice{}).Error
   705  		if err != nil {
   706  			return err
   707  		}
   708  	}
   709  	if !tx.HasTable(tableNameLineItem) {
   710  		err := tx.CreateTable(&LineItem{}).Error
   711  		if err != nil {
   712  			return err
   713  		}
   714  	}
   715  	if !tx.HasTable(tableNameInvoiceChange) {
   716  		err := tx.CreateTable(&InvoiceChange{}).Error
   717  		if err != nil {
   718  			return err
   719  		}
   720  	}
   721  	if !tx.HasTable(tableNameExchangeRate) {
   722  		err := tx.CreateTable(&ExchangeRate{}).Error
   723  		if err != nil {
   724  			return err
   725  		}
   726  	}
   727  	if !tx.HasTable(tableNamePayments) {
   728  		err := tx.CreateTable(&Payments{}).Error
   729  		if err != nil {
   730  			return err
   731  		}
   732  	}
   733  	if !tx.HasTable(tableNameDCC) {
   734  		err := tx.CreateTable(&DCC{}).Error
   735  		if err != nil {
   736  			return err
   737  		}
   738  	}
   739  	if !tx.HasTable(tableNameVersions) {
   740  		err := tx.CreateTable(&Version{}).Error
   741  		if err != nil {
   742  			return err
   743  		}
   744  	}
   745  
   746  	var v Version
   747  	err := tx.Where("id = ?", cacheID).
   748  		Find(&v).
   749  		Error
   750  	if errors.Is(err, gorm.ErrRecordNotFound) {
   751  		err = tx.Create(
   752  			&Version{
   753  				ID:        cacheID,
   754  				Version:   cmsVersion,
   755  				Timestamp: time.Now().Unix(),
   756  			}).Error
   757  		return err
   758  	}
   759  	return nil
   760  }
   761  
   762  // Setup calls the tables creation function to ensure the database is prepared for use.
   763  func (c *cockroachdb) Setup() error {
   764  	tx := c.recordsdb.Begin()
   765  	err := createCmsTables(tx)
   766  	if err != nil {
   767  		tx.Rollback()
   768  		return err
   769  	}
   770  
   771  	return tx.Commit().Error
   772  }
   773  
   774  func (c *cockroachdb) dropTables(tx *gorm.DB) error {
   775  	// Drop record tables
   776  	err := tx.DropTableIfExists(tableNameInvoice, tableNameInvoiceChange,
   777  		tableNameLineItem, tableNamePayments, tableNameDCC).Error
   778  	if err != nil {
   779  		return err
   780  	}
   781  
   782  	// Remove cms version record
   783  	return tx.Delete(&Version{
   784  		ID: cacheID,
   785  	}).Error
   786  }
   787  
   788  // build the records cache using the passed in records.
   789  //
   790  // This function cannot be called using a transaction because it could
   791  // potentially exceed cockroachdb's transaction size limit.
   792  func (c *cockroachdb) build(invoices []Invoice, dccs []DCC) error {
   793  	log.Tracef("build")
   794  
   795  	// Drop record tables
   796  	tx := c.recordsdb.Begin()
   797  	err := c.dropTables(tx)
   798  	if err != nil {
   799  		tx.Rollback()
   800  		return fmt.Errorf("drop tables: %v", err)
   801  	}
   802  	err = tx.Commit().Error
   803  	if err != nil {
   804  		return err
   805  	}
   806  
   807  	// Create record tables
   808  	tx = c.recordsdb.Begin()
   809  	err = createCmsTables(tx)
   810  	if err != nil {
   811  		tx.Rollback()
   812  		return fmt.Errorf("create tables: %v", err)
   813  	}
   814  	err = tx.Commit().Error
   815  	if err != nil {
   816  		return err
   817  	}
   818  
   819  	for _, i := range invoices {
   820  		err := c.recordsdb.Create(&i).Error
   821  		if err != nil {
   822  			return fmt.Errorf("create invoice: %v", err)
   823  		}
   824  	}
   825  
   826  	for _, d := range dccs {
   827  		err := c.recordsdb.Create(&d).Error
   828  		if err != nil {
   829  			return fmt.Errorf("create dcc: %v", err)
   830  		}
   831  	}
   832  
   833  	return nil
   834  }
   835  
   836  // Build drops all existing tables from the records cache, recreates them, then
   837  // builds the records cache using the passed in records.
   838  func (c *cockroachdb) Build(dbInvs []database.Invoice, dbDCCs []database.DCC) error {
   839  	log.Tracef("Build")
   840  
   841  	c.Lock()
   842  	defer c.Unlock()
   843  
   844  	if c.shutdown {
   845  		return database.ErrShutdown
   846  	}
   847  
   848  	log.Infof("Building records cache")
   849  	invoices := make([]Invoice, 0, len(dbInvs))
   850  	for _, dbInv := range dbInvs {
   851  		inv := EncodeInvoice(&dbInv)
   852  		invoices = append(invoices, *inv)
   853  	}
   854  	dccs := make([]DCC, 0, len(dbDCCs))
   855  	for _, dbDCC := range dbDCCs {
   856  		dcc := encodeDCC(&dbDCC)
   857  		dccs = append(dccs, *dcc)
   858  	}
   859  	// Build the records cache. This is not run using a
   860  	// transaction because it could potentially exceed
   861  	// cockroachdb's transaction size limit.
   862  	err := c.build(invoices, dccs)
   863  	if err != nil {
   864  		// Remove the version record. This will
   865  		// force a rebuild on the next start up.
   866  		err1 := c.recordsdb.Delete(&Version{
   867  			ID: cacheID,
   868  		}).Error
   869  		if err1 != nil {
   870  			panic("the cache is out of sync and will not rebuild" +
   871  				"automatically; a rebuild must be forced")
   872  		}
   873  	}
   874  
   875  	return err
   876  }
   877  
   878  func buildQueryString(user, rootCert, cert, key string) string {
   879  	v := url.Values{}
   880  	v.Set("sslmode", "require")
   881  	v.Set("sslrootcert", filepath.Clean(rootCert))
   882  	v.Set("sslcert", filepath.Join(cert))
   883  	v.Set("sslkey", filepath.Join(key))
   884  	return v.Encode()
   885  }
   886  
   887  // New returns a new cockroachdb context that contains a connection to the
   888  // specified database that was made using the politeiawww user and the passed
   889  // in certificates.
   890  func New(host, net, rootCert, cert, key string) (*cockroachdb, error) {
   891  	log.Tracef("New: %v %v %v %v %v", host, net, rootCert, cert, key)
   892  
   893  	// Connect to database
   894  	dbName := cacheID + "_" + net
   895  	h := "postgresql://" + userPoliteiawww + "@" + host + "/" + dbName
   896  	u, err := url.Parse(h)
   897  	if err != nil {
   898  		return nil, fmt.Errorf("parse url '%v': %v", h, err)
   899  	}
   900  
   901  	qs := buildQueryString(u.User.String(), rootCert, cert, key)
   902  	addr := u.String() + "?" + qs
   903  	db, err := gorm.Open("postgres", addr)
   904  	if err != nil {
   905  		return nil, fmt.Errorf("connect to database '%v': %v", addr, err)
   906  	}
   907  
   908  	// Create context
   909  	c := &cockroachdb{
   910  		recordsdb: db,
   911  	}
   912  
   913  	// Disable gorm logging. This prevents duplicate errors from
   914  	// being printed since we handle errors manually.
   915  	c.recordsdb.LogMode(false)
   916  
   917  	// Disable automatic table name pluralization. We set table
   918  	// names manually.
   919  	c.recordsdb.SingularTable(true)
   920  
   921  	// Return an error if the version record is not found or
   922  	// if there is a version mismatch, but also return the
   923  	// cache context so that the cache can be built/rebuilt.
   924  	if !c.recordsdb.HasTable(tableNameVersions) {
   925  		log.Debugf("table '%v' does not exist", tableNameVersions)
   926  		return c, database.ErrNoVersionRecord
   927  	}
   928  
   929  	var v Version
   930  	err = c.recordsdb.
   931  		Where("id = ?", cacheID).
   932  		Find(&v).
   933  		Error
   934  	if errors.Is(err, gorm.ErrRecordNotFound) {
   935  		log.Debugf("version record not found for ID '%v'", cacheID)
   936  		err = database.ErrNoVersionRecord
   937  	} else if v.Version != cmsVersion {
   938  		log.Debugf("version mismatch for ID '%v': got %v, want %v",
   939  			cacheID, v.Version, cmsVersion)
   940  		err = database.ErrWrongVersion
   941  	}
   942  
   943  	return c, err
   944  }
   945  
   946  // Update existing payment.
   947  //
   948  // UpdatePayments satisfies the database interface.
   949  func (c *cockroachdb) UpdatePayments(dbPayments *database.Payments) error {
   950  	payments := encodePayments(dbPayments)
   951  
   952  	log.Debugf("UpdatePayments: %v", payments.InvoiceKey)
   953  
   954  	return c.recordsdb.Save(&payments).Error
   955  }
   956  
   957  // PaymentsByAddress returns payments row that has the matching Address.
   958  func (c *cockroachdb) PaymentsByAddress(address string) (*database.Payments, error) {
   959  	log.Debugf("PaymentsByAddress: %v", address)
   960  
   961  	payments := Payments{}
   962  	err := c.recordsdb.
   963  		Where("address = ?", address).
   964  		Find(&payments).Error
   965  	if err != nil {
   966  		if errors.Is(err, gorm.ErrRecordNotFound) {
   967  			err = database.ErrInvoiceNotFound
   968  		}
   969  		return nil, err
   970  	}
   971  	dbPayments := decodePayment(&payments)
   972  	return &dbPayments, nil
   973  }
   974  
   975  // PaymentsByStatus returns all payments rows that match the given status.
   976  func (c *cockroachdb) PaymentsByStatus(status uint) ([]database.Payments, error) {
   977  	log.Debugf("PaymentsByStatus: %v", status)
   978  
   979  	payments := make([]Payments, 0, 1048)
   980  	err := c.recordsdb.
   981  		Where("status = ?", status).
   982  		Find(&payments).
   983  		Error
   984  	if err != nil {
   985  		if errors.Is(err, gorm.ErrRecordNotFound) {
   986  			err = database.ErrInvoiceNotFound
   987  		}
   988  		return nil, err
   989  	}
   990  	dbPayments := make([]database.Payments, 0, 1048)
   991  	for _, v := range payments {
   992  		dbPayments = append(dbPayments, decodePayment(&v))
   993  	}
   994  	return dbPayments, nil
   995  }
   996  
   997  // Create new dcc.
   998  //
   999  // NewDCC satisfies the database interface.
  1000  func (c *cockroachdb) NewDCC(dbDCC *database.DCC) error {
  1001  	dcc := encodeDCC(dbDCC)
  1002  
  1003  	log.Debugf("NewDCC: %v", dcc.Token)
  1004  	return c.recordsdb.Create(dcc).Error
  1005  }
  1006  
  1007  // Update existing dcc.
  1008  //
  1009  // UpdateDCC satisfies the database interface.
  1010  func (c *cockroachdb) UpdateDCC(dbDCC *database.DCC) error {
  1011  	dcc := encodeDCC(dbDCC)
  1012  
  1013  	log.Debugf("UpdateDCC: %v", dcc.Token)
  1014  
  1015  	return c.recordsdb.Save(dcc).Error
  1016  }
  1017  
  1018  // DCCByToken Return DCC by its token.
  1019  func (c *cockroachdb) DCCByToken(token string) (*database.DCC, error) {
  1020  	log.Debugf("DCCByToken: %v", token)
  1021  
  1022  	dcc := DCC{
  1023  		Token: token,
  1024  	}
  1025  	err := c.recordsdb.
  1026  		Find(&dcc).Error
  1027  	if err != nil {
  1028  		if errors.Is(err, gorm.ErrRecordNotFound) {
  1029  			err = database.ErrDCCNotFound
  1030  		}
  1031  		return nil, err
  1032  	}
  1033  
  1034  	return decodeDCC(&dcc), nil
  1035  }
  1036  
  1037  // DCCsByStatus Return DCCs by status.
  1038  func (c *cockroachdb) DCCsByStatus(status int) ([]*database.DCC, error) {
  1039  	log.Debugf("DCCsByStatus: %v", status)
  1040  
  1041  	dccs := make([]DCC, 0, 1048)
  1042  	err := c.recordsdb.
  1043  		Where("status = ?", status).
  1044  		Find(&dccs).
  1045  		Error
  1046  	if err != nil {
  1047  		if errors.Is(err, gorm.ErrRecordNotFound) {
  1048  			err = database.ErrDCCNotFound
  1049  		}
  1050  		return nil, err
  1051  	}
  1052  
  1053  	dbDCCs := make([]*database.DCC, 0, 1048)
  1054  	for _, v := range dccs {
  1055  		dbDCCs = append(dbDCCs, decodeDCC(&v))
  1056  	}
  1057  	return dbDCCs, nil
  1058  }
  1059  
  1060  // DCCsAll Returns all DCCs regardless of status.
  1061  func (c *cockroachdb) DCCsAll() ([]*database.DCC, error) {
  1062  	log.Debugf("DCCsAll:")
  1063  
  1064  	dccs := make([]DCC, 0, 1048)
  1065  	err := c.recordsdb.
  1066  		Find(&dccs).
  1067  		Error
  1068  	if err != nil {
  1069  		if errors.Is(err, gorm.ErrRecordNotFound) {
  1070  			err = database.ErrDCCNotFound
  1071  		}
  1072  		return nil, err
  1073  	}
  1074  
  1075  	dbDCCs := make([]*database.DCC, 0, 1048)
  1076  	for _, v := range dccs {
  1077  		dbDCCs = append(dbDCCs, decodeDCC(&v))
  1078  	}
  1079  	return dbDCCs, nil
  1080  }