1
0
Fork 0
golang-github-meilisearch-m.../meilisearch.go
Daniel Baumann 5d4914ed7f
Adding upstream version 0.31.1.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-18 21:42:39 +02:00

717 lines
21 KiB
Go

package meilisearch
import (
"context"
"fmt"
"net/http"
"strconv"
"time"
"github.com/golang-jwt/jwt/v4"
)
type meilisearch struct {
client *client
}
// New create new service manager for operating on meilisearch
func New(host string, options ...Option) ServiceManager {
defOpt := defaultMeiliOpt
for _, opt := range options {
opt(defOpt)
}
return &meilisearch{
client: newClient(
defOpt.client,
host,
defOpt.apiKey,
clientConfig{
contentEncoding: defOpt.contentEncoding.encodingType,
encodingCompressionLevel: defOpt.contentEncoding.level,
disableRetry: defOpt.disableRetry,
retryOnStatus: defOpt.retryOnStatus,
maxRetries: defOpt.maxRetries,
},
),
}
}
// Connect create service manager and check connection with meilisearch
func Connect(host string, options ...Option) (ServiceManager, error) {
meili := New(host, options...)
if !meili.IsHealthy() {
return nil, ErrConnectingFailed
}
return meili, nil
}
func (m *meilisearch) ServiceReader() ServiceReader {
return m
}
func (m *meilisearch) TaskManager() TaskManager {
return m
}
func (m *meilisearch) TaskReader() TaskReader {
return m
}
func (m *meilisearch) KeyManager() KeyManager {
return m
}
func (m *meilisearch) KeyReader() KeyReader {
return m
}
func (m *meilisearch) Index(uid string) IndexManager {
return newIndex(m.client, uid)
}
func (m *meilisearch) GetIndex(indexID string) (*IndexResult, error) {
return m.GetIndexWithContext(context.Background(), indexID)
}
func (m *meilisearch) GetIndexWithContext(ctx context.Context, indexID string) (*IndexResult, error) {
return newIndex(m.client, indexID).FetchInfoWithContext(ctx)
}
func (m *meilisearch) GetRawIndex(uid string) (map[string]interface{}, error) {
return m.GetRawIndexWithContext(context.Background(), uid)
}
func (m *meilisearch) GetRawIndexWithContext(ctx context.Context, uid string) (map[string]interface{}, error) {
resp := map[string]interface{}{}
req := &internalRequest{
endpoint: "/indexes/" + uid,
method: http.MethodGet,
withRequest: nil,
withResponse: &resp,
acceptedStatusCodes: []int{http.StatusOK},
functionName: "GetRawIndex",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) ListIndexes(param *IndexesQuery) (*IndexesResults, error) {
return m.ListIndexesWithContext(context.Background(), param)
}
func (m *meilisearch) ListIndexesWithContext(ctx context.Context, param *IndexesQuery) (*IndexesResults, error) {
resp := new(IndexesResults)
req := &internalRequest{
endpoint: "/indexes",
method: http.MethodGet,
withRequest: nil,
withResponse: &resp,
withQueryParams: map[string]string{},
acceptedStatusCodes: []int{http.StatusOK},
functionName: "GetIndexes",
}
if param != nil && param.Limit != 0 {
req.withQueryParams["limit"] = strconv.FormatInt(param.Limit, 10)
}
if param != nil && param.Offset != 0 {
req.withQueryParams["offset"] = strconv.FormatInt(param.Offset, 10)
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
for i := range resp.Results {
resp.Results[i].IndexManager = newIndex(m.client, resp.Results[i].UID)
}
return resp, nil
}
func (m *meilisearch) GetRawIndexes(param *IndexesQuery) (map[string]interface{}, error) {
return m.GetRawIndexesWithContext(context.Background(), param)
}
func (m *meilisearch) GetRawIndexesWithContext(ctx context.Context, param *IndexesQuery) (map[string]interface{}, error) {
resp := map[string]interface{}{}
req := &internalRequest{
endpoint: "/indexes",
method: http.MethodGet,
withRequest: nil,
withResponse: &resp,
withQueryParams: map[string]string{},
acceptedStatusCodes: []int{http.StatusOK},
functionName: "GetRawIndexes",
}
if param != nil && param.Limit != 0 {
req.withQueryParams["limit"] = strconv.FormatInt(param.Limit, 10)
}
if param != nil && param.Offset != 0 {
req.withQueryParams["offset"] = strconv.FormatInt(param.Offset, 10)
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) CreateIndex(config *IndexConfig) (*TaskInfo, error) {
return m.CreateIndexWithContext(context.Background(), config)
}
func (m *meilisearch) CreateIndexWithContext(ctx context.Context, config *IndexConfig) (*TaskInfo, error) {
request := &CreateIndexRequest{
UID: config.Uid,
PrimaryKey: config.PrimaryKey,
}
resp := new(TaskInfo)
req := &internalRequest{
endpoint: "/indexes",
method: http.MethodPost,
contentType: contentTypeJSON,
withRequest: request,
withResponse: resp,
acceptedStatusCodes: []int{http.StatusAccepted},
functionName: "CreateIndex",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) DeleteIndex(uid string) (*TaskInfo, error) {
return m.DeleteIndexWithContext(context.Background(), uid)
}
func (m *meilisearch) DeleteIndexWithContext(ctx context.Context, uid string) (*TaskInfo, error) {
resp := new(TaskInfo)
req := &internalRequest{
endpoint: "/indexes/" + uid,
method: http.MethodDelete,
withRequest: nil,
withResponse: resp,
acceptedStatusCodes: []int{http.StatusAccepted},
functionName: "DeleteIndex",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) MultiSearch(queries *MultiSearchRequest) (*MultiSearchResponse, error) {
return m.MultiSearchWithContext(context.Background(), queries)
}
func (m *meilisearch) MultiSearchWithContext(ctx context.Context, queries *MultiSearchRequest) (*MultiSearchResponse, error) {
resp := new(MultiSearchResponse)
for i := 0; i < len(queries.Queries); i++ {
queries.Queries[i].validate()
}
req := &internalRequest{
endpoint: "/multi-search",
method: http.MethodPost,
contentType: contentTypeJSON,
withRequest: queries,
withResponse: resp,
acceptedStatusCodes: []int{http.StatusOK},
functionName: "MultiSearch",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) CreateKey(request *Key) (*Key, error) {
return m.CreateKeyWithContext(context.Background(), request)
}
func (m *meilisearch) CreateKeyWithContext(ctx context.Context, request *Key) (*Key, error) {
parsedRequest := convertKeyToParsedKey(*request)
resp := new(Key)
req := &internalRequest{
endpoint: "/keys",
method: http.MethodPost,
contentType: contentTypeJSON,
withRequest: &parsedRequest,
withResponse: resp,
acceptedStatusCodes: []int{http.StatusCreated},
functionName: "CreateKey",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) GetKey(identifier string) (*Key, error) {
return m.GetKeyWithContext(context.Background(), identifier)
}
func (m *meilisearch) GetKeyWithContext(ctx context.Context, identifier string) (*Key, error) {
resp := new(Key)
req := &internalRequest{
endpoint: "/keys/" + identifier,
method: http.MethodGet,
withRequest: nil,
withResponse: resp,
acceptedStatusCodes: []int{http.StatusOK},
functionName: "GetKey",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) GetKeys(param *KeysQuery) (*KeysResults, error) {
return m.GetKeysWithContext(context.Background(), param)
}
func (m *meilisearch) GetKeysWithContext(ctx context.Context, param *KeysQuery) (*KeysResults, error) {
resp := new(KeysResults)
req := &internalRequest{
endpoint: "/keys",
method: http.MethodGet,
withRequest: nil,
withResponse: resp,
withQueryParams: map[string]string{},
acceptedStatusCodes: []int{http.StatusOK},
functionName: "GetKeys",
}
if param != nil && param.Limit != 0 {
req.withQueryParams["limit"] = strconv.FormatInt(param.Limit, 10)
}
if param != nil && param.Offset != 0 {
req.withQueryParams["offset"] = strconv.FormatInt(param.Offset, 10)
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) UpdateKey(keyOrUID string, request *Key) (*Key, error) {
return m.UpdateKeyWithContext(context.Background(), keyOrUID, request)
}
func (m *meilisearch) UpdateKeyWithContext(ctx context.Context, keyOrUID string, request *Key) (*Key, error) {
parsedRequest := KeyUpdate{Name: request.Name, Description: request.Description}
resp := new(Key)
req := &internalRequest{
endpoint: "/keys/" + keyOrUID,
method: http.MethodPatch,
contentType: contentTypeJSON,
withRequest: &parsedRequest,
withResponse: resp,
acceptedStatusCodes: []int{http.StatusOK},
functionName: "UpdateKey",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) DeleteKey(keyOrUID string) (bool, error) {
return m.DeleteKeyWithContext(context.Background(), keyOrUID)
}
func (m *meilisearch) DeleteKeyWithContext(ctx context.Context, keyOrUID string) (bool, error) {
req := &internalRequest{
endpoint: "/keys/" + keyOrUID,
method: http.MethodDelete,
withRequest: nil,
withResponse: nil,
acceptedStatusCodes: []int{http.StatusNoContent},
functionName: "DeleteKey",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return false, err
}
return true, nil
}
func (m *meilisearch) GetTask(taskUID int64) (*Task, error) {
return m.GetTaskWithContext(context.Background(), taskUID)
}
func (m *meilisearch) GetTaskWithContext(ctx context.Context, taskUID int64) (*Task, error) {
return getTask(ctx, m.client, taskUID)
}
func (m *meilisearch) GetTasks(param *TasksQuery) (*TaskResult, error) {
return m.GetTasksWithContext(context.Background(), param)
}
func (m *meilisearch) GetTasksWithContext(ctx context.Context, param *TasksQuery) (*TaskResult, error) {
resp := new(TaskResult)
req := &internalRequest{
endpoint: "/tasks",
method: http.MethodGet,
withRequest: nil,
withResponse: &resp,
withQueryParams: map[string]string{},
acceptedStatusCodes: []int{http.StatusOK},
functionName: "GetTasks",
}
if param != nil {
encodeTasksQuery(param, req)
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) CancelTasks(param *CancelTasksQuery) (*TaskInfo, error) {
return m.CancelTasksWithContext(context.Background(), param)
}
func (m *meilisearch) CancelTasksWithContext(ctx context.Context, param *CancelTasksQuery) (*TaskInfo, error) {
resp := new(TaskInfo)
req := &internalRequest{
endpoint: "/tasks/cancel",
method: http.MethodPost,
withRequest: nil,
withResponse: &resp,
withQueryParams: map[string]string{},
acceptedStatusCodes: []int{http.StatusOK},
functionName: "CancelTasks",
}
if param != nil {
paramToSend := &TasksQuery{
UIDS: param.UIDS,
IndexUIDS: param.IndexUIDS,
Statuses: param.Statuses,
Types: param.Types,
BeforeEnqueuedAt: param.BeforeEnqueuedAt,
AfterEnqueuedAt: param.AfterEnqueuedAt,
BeforeStartedAt: param.BeforeStartedAt,
AfterStartedAt: param.AfterStartedAt,
}
encodeTasksQuery(paramToSend, req)
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) DeleteTasks(param *DeleteTasksQuery) (*TaskInfo, error) {
return m.DeleteTasksWithContext(context.Background(), param)
}
func (m *meilisearch) DeleteTasksWithContext(ctx context.Context, param *DeleteTasksQuery) (*TaskInfo, error) {
resp := new(TaskInfo)
req := &internalRequest{
endpoint: "/tasks",
method: http.MethodDelete,
withRequest: nil,
withResponse: &resp,
withQueryParams: map[string]string{},
acceptedStatusCodes: []int{http.StatusOK},
functionName: "DeleteTasks",
}
if param != nil {
paramToSend := &TasksQuery{
UIDS: param.UIDS,
IndexUIDS: param.IndexUIDS,
Statuses: param.Statuses,
Types: param.Types,
CanceledBy: param.CanceledBy,
BeforeEnqueuedAt: param.BeforeEnqueuedAt,
AfterEnqueuedAt: param.AfterEnqueuedAt,
BeforeStartedAt: param.BeforeStartedAt,
AfterStartedAt: param.AfterStartedAt,
BeforeFinishedAt: param.BeforeFinishedAt,
AfterFinishedAt: param.AfterFinishedAt,
}
encodeTasksQuery(paramToSend, req)
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) SwapIndexes(param []*SwapIndexesParams) (*TaskInfo, error) {
return m.SwapIndexesWithContext(context.Background(), param)
}
func (m *meilisearch) SwapIndexesWithContext(ctx context.Context, param []*SwapIndexesParams) (*TaskInfo, error) {
resp := new(TaskInfo)
req := &internalRequest{
endpoint: "/swap-indexes",
method: http.MethodPost,
contentType: contentTypeJSON,
withRequest: param,
withResponse: &resp,
acceptedStatusCodes: []int{http.StatusAccepted},
functionName: "SwapIndexes",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) WaitForTask(taskUID int64, interval time.Duration) (*Task, error) {
return waitForTask(context.Background(), m.client, taskUID, interval)
}
func (m *meilisearch) WaitForTaskWithContext(ctx context.Context, taskUID int64, interval time.Duration) (*Task, error) {
return waitForTask(ctx, m.client, taskUID, interval)
}
func (m *meilisearch) GenerateTenantToken(
apiKeyUID string,
searchRules map[string]interface{},
options *TenantTokenOptions,
) (string, error) {
// validate the arguments
if searchRules == nil {
return "", fmt.Errorf("GenerateTenantToken: The search rules added in the token generation " +
"must be of type array or object")
}
if (options == nil || options.APIKey == "") && m.client.apiKey == "" {
return "", fmt.Errorf("GenerateTenantToken: The API key used for the token " +
"generation must exist and be a valid meilisearch key")
}
if apiKeyUID == "" || !IsValidUUID(apiKeyUID) {
return "", fmt.Errorf("GenerateTenantToken: The uid used for the token " +
"generation must exist and comply to uuid4 format")
}
if options != nil && !options.ExpiresAt.IsZero() && options.ExpiresAt.Before(time.Now()) {
return "", fmt.Errorf("GenerateTenantToken: When the expiresAt field in " +
"the token generation has a value, it must be a date set in the future")
}
var secret string
if options == nil || options.APIKey == "" {
secret = m.client.apiKey
} else {
secret = options.APIKey
}
// For HMAC signing method, the key should be any []byte
hmacSampleSecret := []byte(secret)
// Create the claims
claims := TenantTokenClaims{}
if options != nil && !options.ExpiresAt.IsZero() {
claims.RegisteredClaims = jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(options.ExpiresAt),
}
}
claims.APIKeyUID = apiKeyUID
claims.SearchRules = searchRules
// Create a new token object, specifying signing method and the claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Sign and get the complete encoded token as a string using the secret
tokenString, err := token.SignedString(hmacSampleSecret)
return tokenString, err
}
func (m *meilisearch) GetStats() (*Stats, error) {
return m.GetStatsWithContext(context.Background())
}
func (m *meilisearch) GetStatsWithContext(ctx context.Context) (*Stats, error) {
resp := new(Stats)
req := &internalRequest{
endpoint: "/stats",
method: http.MethodGet,
withRequest: nil,
withResponse: resp,
acceptedStatusCodes: []int{http.StatusOK},
functionName: "GetStats",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) CreateDump() (*TaskInfo, error) {
return m.CreateDumpWithContext(context.Background())
}
func (m *meilisearch) CreateDumpWithContext(ctx context.Context) (*TaskInfo, error) {
resp := new(TaskInfo)
req := &internalRequest{
endpoint: "/dumps",
method: http.MethodPost,
contentType: contentTypeJSON,
withRequest: nil,
withResponse: resp,
acceptedStatusCodes: []int{http.StatusAccepted},
functionName: "CreateDump",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) Version() (*Version, error) {
return m.VersionWithContext(context.Background())
}
func (m *meilisearch) VersionWithContext(ctx context.Context) (*Version, error) {
resp := new(Version)
req := &internalRequest{
endpoint: "/version",
method: http.MethodGet,
withRequest: nil,
withResponse: resp,
acceptedStatusCodes: []int{http.StatusOK},
functionName: "Version",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) Health() (*Health, error) {
return m.HealthWithContext(context.Background())
}
func (m *meilisearch) HealthWithContext(ctx context.Context) (*Health, error) {
resp := new(Health)
req := &internalRequest{
endpoint: "/health",
method: http.MethodGet,
withRequest: nil,
withResponse: resp,
acceptedStatusCodes: []int{http.StatusOK},
functionName: "Health",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) CreateSnapshot() (*TaskInfo, error) {
return m.CreateSnapshotWithContext(context.Background())
}
func (m *meilisearch) CreateSnapshotWithContext(ctx context.Context) (*TaskInfo, error) {
resp := new(TaskInfo)
req := &internalRequest{
endpoint: "/snapshots",
method: http.MethodPost,
withRequest: nil,
withResponse: resp,
acceptedStatusCodes: []int{http.StatusAccepted},
contentType: contentTypeJSON,
functionName: "CreateSnapshot",
}
if err := m.client.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func (m *meilisearch) IsHealthy() bool {
res, err := m.HealthWithContext(context.Background())
return err == nil && res.Status == "available"
}
func (m *meilisearch) Close() {
m.client.client.CloseIdleConnections()
}
func getTask(ctx context.Context, cli *client, taskUID int64) (*Task, error) {
resp := new(Task)
req := &internalRequest{
endpoint: "/tasks/" + strconv.FormatInt(taskUID, 10),
method: http.MethodGet,
withRequest: nil,
withResponse: resp,
acceptedStatusCodes: []int{http.StatusOK},
functionName: "GetTask",
}
if err := cli.executeRequest(ctx, req); err != nil {
return nil, err
}
return resp, nil
}
func waitForTask(ctx context.Context, cli *client, taskUID int64, interval time.Duration) (*Task, error) {
if interval == 0 {
interval = 50 * time.Millisecond
}
// extract closure to get the task and check the status first before the ticker
fn := func() (*Task, error) {
getTask, err := getTask(ctx, cli, taskUID)
if err != nil {
return nil, err
}
if getTask.Status != TaskStatusEnqueued && getTask.Status != TaskStatusProcessing {
return getTask, nil
}
return nil, nil
}
// run first before the ticker, we do not want to wait for the first interval
task, err := fn()
if err != nil {
// Return error if it exists
return nil, err
}
// Return task if it exists
if task != nil {
return task, nil
}
// Create a ticker to check the task status, because our initial check was not successful
ticker := time.NewTicker(interval)
// Defer the stop of the ticker, help GC to cleanup
defer func() {
// we might want to revist this, go.mod now is 1.16
// however I still encouter the issue on go 1.22.2
// there are 2 issues regarding tickers
// https://go-review.googlesource.com/c/go/+/512355
// https://github.com/golang/go/issues/61542
ticker.Stop()
ticker = nil
}()
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-ticker.C:
task, err := fn()
if err != nil {
return nil, err
}
if task != nil {
return task, nil
}
}
}
}