diff --git a/cli/cli.go b/cli/cli.go index 0875e325fe..4453cbaafb 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -111,6 +111,7 @@ func NewDefraCommand() *cobra.Command { client := MakeClientCommand() client.AddCommand( + MakePurgeCommand(), MakeDumpCommand(), MakeRequestCommand(), schema, diff --git a/cli/config.go b/cli/config.go index 9a0290eb04..745d43e617 100644 --- a/cli/config.go +++ b/cli/config.go @@ -64,6 +64,7 @@ var configFlags = map[string]string{ "keyring-path": "keyring.path", "no-keyring": "keyring.disabled", "source-hub-address": "acp.sourceHub.address", + "development": "development", } // configDefaults contains default values for config entries. @@ -74,6 +75,7 @@ var configDefaults = map[string]any{ "datastore.maxtxnretries": 5, "datastore.store": "badger", "datastore.badger.valuelogfilesize": 1 << 30, + "development": false, "net.p2pdisabled": false, "net.p2paddresses": []string{"/ip4/127.0.0.1/tcp/9171"}, "net.peers": []string{}, diff --git a/cli/config_test.go b/cli/config_test.go index d3f6d954e3..36421bd42f 100644 --- a/cli/config_test.go +++ b/cli/config_test.go @@ -68,4 +68,6 @@ func TestLoadConfigNotExist(t *testing.T) { assert.Equal(t, false, cfg.GetBool("keyring.disabled")) assert.Equal(t, "defradb", cfg.GetString("keyring.namespace")) assert.Equal(t, "file", cfg.GetString("keyring.backend")) + + assert.Equal(t, false, cfg.GetBool("development")) } diff --git a/cli/errors.go b/cli/errors.go index 504cb9ca25..c22957ff51 100644 --- a/cli/errors.go +++ b/cli/errors.go @@ -40,6 +40,7 @@ var ( ErrSchemaVersionNotOfSchema = errors.New(errSchemaVersionNotOfSchema) ErrViewAddMissingArgs = errors.New("please provide a base query and output SDL for this view") ErrPolicyFileArgCanNotBeEmpty = errors.New("policy file argument can not be empty") + ErrPurgeForceFlagRequired = errors.New("run this command again with --force if you really want to purge all data") ) func NewErrRequiredFlagEmpty(longName string, shortName string) error { diff --git a/cli/purge.go b/cli/purge.go new file mode 100644 index 0000000000..5880e021b8 --- /dev/null +++ b/cli/purge.go @@ -0,0 +1,36 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package cli + +import ( + "github.com/spf13/cobra" + + "github.com/sourcenetwork/defradb/http" +) + +func MakePurgeCommand() *cobra.Command { + var force bool + var cmd = &cobra.Command{ + Use: "purge", + Short: "Delete all persisted data and restart", + Long: `Delete all persisted data and restart. +WARNING this operation cannot be reversed.`, + RunE: func(cmd *cobra.Command, args []string) error { + db := mustGetContextDB(cmd).(*http.Client) + if !force { + return ErrPurgeForceFlagRequired + } + return db.Purge(cmd.Context()) + }, + } + cmd.Flags().BoolVarP(&force, "force", "f", false, "Must be set for the operation to run") + return cmd +} diff --git a/cli/purge_test.go b/cli/purge_test.go new file mode 100644 index 0000000000..8a94d2eb21 --- /dev/null +++ b/cli/purge_test.go @@ -0,0 +1,25 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package cli + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPurgeCommandWithoutForceFlagReturnsError(t *testing.T) { + cmd := NewDefraCommand() + cmd.SetArgs([]string{"client", "purge"}) + + err := cmd.Execute() + require.ErrorIs(t, err, ErrPurgeForceFlagRequired) +} diff --git a/cli/start.go b/cli/start.go index 651360ab83..2b5b4fb734 100644 --- a/cli/start.go +++ b/cli/start.go @@ -19,6 +19,7 @@ import ( "github.com/spf13/cobra" "github.com/sourcenetwork/defradb/errors" + "github.com/sourcenetwork/defradb/event" "github.com/sourcenetwork/defradb/http" "github.com/sourcenetwork/defradb/internal/db" "github.com/sourcenetwork/defradb/keyring" @@ -26,6 +27,17 @@ import ( "github.com/sourcenetwork/defradb/node" ) +const devModeBanner = ` +****************************************** +** DEVELOPMENT MODE IS ENABLED ** +** ------------------------------------ ** +** if this is a production database ** +** disable development mode and restart ** +** or you may risk losing all data ** +****************************************** + +` + func MakeStartCommand() *cobra.Command { var cmd = &cobra.Command{ Use: "start", @@ -46,12 +58,15 @@ func MakeStartCommand() *cobra.Command { cfg := mustGetContextConfig(cmd) opts := []node.Option{ - node.WithStorePath(cfg.GetString("datastore.badger.path")), - node.WithBadgerInMemory(cfg.GetString("datastore.store") == configStoreMemory), node.WithDisableP2P(cfg.GetBool("net.p2pDisabled")), node.WithSourceHubChainID(cfg.GetString("acp.sourceHub.ChainID")), node.WithSourceHubGRPCAddress(cfg.GetString("acp.sourceHub.GRPCAddress")), node.WithSourceHubCometRPCAddress(cfg.GetString("acp.sourceHub.CometRPCAddress")), + node.WithLensRuntime(node.LensRuntimeType(cfg.GetString("lens.runtime"))), + node.WithEnableDevelopment(cfg.GetBool("development")), + // store options + node.WithStorePath(cfg.GetString("datastore.badger.path")), + node.WithBadgerInMemory(cfg.GetString("datastore.store") == configStoreMemory), // db options db.WithMaxRetries(cfg.GetInt("datastore.MaxTxnRetries")), // net node options @@ -64,7 +79,6 @@ func MakeStartCommand() *cobra.Command { http.WithAllowedOrigins(cfg.GetStringSlice("api.allowed-origins")...), http.WithTLSCertPath(cfg.GetString("api.pubKeyPath")), http.WithTLSKeyPath(cfg.GetString("api.privKeyPath")), - node.WithLensRuntime(node.LensRuntimeType(cfg.GetString("lens.runtime"))), } if cfg.GetString("datastore.store") != configStoreMemory { @@ -75,6 +89,11 @@ func MakeStartCommand() *cobra.Command { opts = append(opts, node.WithACPPath(rootDir)) } + acpType := cfg.GetString("acp.type") + if acpType != "" { + opts = append(opts, node.WithACPType(node.ACPType(acpType))) + } + if !cfg.GetBool("keyring.disabled") { kr, err := openKeyring(cmd) if err != nil { @@ -91,9 +110,8 @@ func MakeStartCommand() *cobra.Command { if err != nil && !errors.Is(err, keyring.ErrNotFound) { return err } - opts = append(opts, node.WithBadgerEncryptionKey(encryptionKey)) - + // setup the sourcehub transaction signer sourceHubKeyName := cfg.GetString("acp.sourceHub.KeyName") if sourceHubKeyName != "" { signer, err := keyring.NewTxSignerFromKeyringKey(kr, sourceHubKeyName) @@ -104,38 +122,54 @@ func MakeStartCommand() *cobra.Command { } } - acpType := cfg.GetString("acp.type") - if acpType != "" { - opts = append(opts, node.WithACPType(node.ACPType(acpType))) + isDevMode := cfg.GetBool("development") + if isDevMode { + cmd.Printf(devModeBanner) } - n, err := node.NewNode(cmd.Context(), opts...) + signalCh := make(chan os.Signal, 1) + signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) + + n, err := node.New(cmd.Context(), opts...) if err != nil { return err } - - defer func() { - if err := n.Close(cmd.Context()); err != nil { - log.ErrorContextE(cmd.Context(), "Stopping DefraDB", err) - } - }() - log.InfoContext(cmd.Context(), "Starting DefraDB") if err := n.Start(cmd.Context()); err != nil { return err } - signalCh := make(chan os.Signal, 1) - signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM) + RESTART: + // after a restart we need to resubscribe + purgeSub, err := n.DB.Events().Subscribe(event.PurgeName) + if err != nil { + return err + } + SELECT: select { + case <-purgeSub.Message(): + log.InfoContext(cmd.Context(), "Received purge event; restarting...") + + err := n.PurgeAndRestart(cmd.Context()) + if err != nil { + log.ErrorContextE(cmd.Context(), "failed to purge", err) + } + if err == nil { + goto RESTART + } + if errors.Is(err, node.ErrPurgeWithDevModeDisabled) { + goto SELECT + } + case <-cmd.Context().Done(): log.InfoContext(cmd.Context(), "Received context cancellation; shutting down...") + case <-signalCh: log.InfoContext(cmd.Context(), "Received interrupt; shutting down...") } - return nil + return n.Close(cmd.Context()) }, } // set default flag values from config @@ -185,5 +219,10 @@ func MakeStartCommand() *cobra.Command { cfg.GetString(configFlags["privkeypath"]), "Path to the private key for tls", ) + cmd.PersistentFlags().Bool( + "development", + cfg.GetBool(configFlags["development"]), + "Enables a set of features that make development easier but should not be enabled in production", + ) return cmd } diff --git a/docs/config.md b/docs/config.md index 6b592059cb..0ac6e5dd52 100644 --- a/docs/config.md +++ b/docs/config.md @@ -4,6 +4,10 @@ The default DefraDB directory is `$HOME/.defradb`. It can be changed via the --r Relative paths are interpreted as being rooted in the DefraDB directory. +## `development` + +Enables a set of features that make development easier but should not be enabled in production. + ## `datastore.store` Store can be badger or memory. Defaults to `badger`. diff --git a/docs/website/references/cli/defradb_client.md b/docs/website/references/cli/defradb_client.md index dfd398f53a..27f840d7ae 100644 --- a/docs/website/references/cli/defradb_client.md +++ b/docs/website/references/cli/defradb_client.md @@ -43,6 +43,7 @@ Execute queries, add schema types, obtain node info, etc. * [defradb client dump](defradb_client_dump.md) - Dump the contents of DefraDB node-side * [defradb client index](defradb_client_index.md) - Manage collections' indexes of a running DefraDB instance * [defradb client p2p](defradb_client_p2p.md) - Interact with the DefraDB P2P system +* [defradb client purge](defradb_client_purge.md) - Delete all persisted data and restart * [defradb client query](defradb_client_query.md) - Send a DefraDB GraphQL query request * [defradb client schema](defradb_client_schema.md) - Interact with the schema system of a DefraDB node * [defradb client tx](defradb_client_tx.md) - Create, commit, and discard DefraDB transactions diff --git a/docs/website/references/cli/defradb_client_purge.md b/docs/website/references/cli/defradb_client_purge.md new file mode 100644 index 0000000000..3a1b4b2738 --- /dev/null +++ b/docs/website/references/cli/defradb_client_purge.md @@ -0,0 +1,45 @@ +## defradb client purge + +Delete all persisted data and restart + +### Synopsis + +Delete all persisted data and restart. +WARNING this operation cannot be reversed. + +``` +defradb client purge [flags] +``` + +### Options + +``` + -f, --force Must be set for the operation to run + -h, --help help for purge +``` + +### Options inherited from parent commands + +``` + -i, --identity string Hex formatted private key used to authenticate with ACP + --keyring-backend string Keyring backend to use. Options are file or system (default "file") + --keyring-namespace string Service name to use when using the system backend (default "defradb") + --keyring-path string Path to store encrypted keys when using the file backend (default "keys") + --log-format string Log format to use. Options are text or json (default "text") + --log-level string Log level to use. Options are debug, info, error, fatal (default "info") + --log-output string Log output path. Options are stderr or stdout. (default "stderr") + --log-overrides string Logger config overrides. Format ,=,...;,... + --log-source Include source location in logs + --log-stacktrace Include stacktrace in error and fatal logs + --no-keyring Disable the keyring and generate ephemeral keys + --no-log-color Disable colored log output + --rootdir string Directory for persistent data (default: $HOME/.defradb) + --source-hub-address string The SourceHub address authorized by the client to make SourceHub transactions on behalf of the actor + --tx uint Transaction ID + --url string URL of HTTP endpoint to listen on or connect to (default "127.0.0.1:9181") +``` + +### SEE ALSO + +* [defradb client](defradb_client.md) - Interact with a DefraDB node + diff --git a/docs/website/references/cli/defradb_start.md b/docs/website/references/cli/defradb_start.md index d71dfb14e4..9b1e5a8d74 100644 --- a/docs/website/references/cli/defradb_start.md +++ b/docs/website/references/cli/defradb_start.md @@ -14,6 +14,7 @@ defradb start [flags] ``` --allowed-origins stringArray List of origins to allow for CORS requests + --development Enables a set of features that make development easier but should not be enabled in production -h, --help help for start --max-txn-retries int Specify the maximum number of retries per transaction (default 5) --no-p2p Disable the peer-to-peer network synchronization system diff --git a/docs/website/references/http/openapi.json b/docs/website/references/http/openapi.json index 470359097e..9a7198495a 100644 --- a/docs/website/references/http/openapi.json +++ b/docs/website/references/http/openapi.json @@ -1711,6 +1711,23 @@ ] } }, + "/purge": { + "post": { + "description": "Purge all persisted data and restart", + "operationId": "purge", + "responses": { + "200": { + "$ref": "#/components/responses/success" + }, + "400": { + "$ref": "#/components/responses/error" + }, + "default": { + "description": "" + } + } + } + }, "/schema": { "get": { "description": "Introspect schema(s) by name, schema root, or version id.", diff --git a/event/event.go b/event/event.go index 9d24a89c10..698cb8dc90 100644 --- a/event/event.go +++ b/event/event.go @@ -39,6 +39,8 @@ const ( P2PTopicCompletedName = Name("p2p-topic-completed") // ReplicatorCompletedName is the name of the replicator completed event. ReplicatorCompletedName = Name("replicator-completed") + // PurgeName is the name of the purge event. + PurgeName = Name("purge") ) // PubSub is an event that is published when diff --git a/http/client.go b/http/client.go index 0c59e23757..ba272572b8 100644 --- a/http/client.go +++ b/http/client.go @@ -472,6 +472,17 @@ func (c *Client) PrintDump(ctx context.Context) error { return err } +func (c *Client) Purge(ctx context.Context) error { + methodURL := c.http.baseURL.JoinPath("purge") + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, methodURL.String(), nil) + if err != nil { + return err + } + _, err = c.http.request(req) + return err +} + func (c *Client) Close() { // do nothing } diff --git a/http/handler.go b/http/handler.go index 3ec33d9b2a..cdb09767c6 100644 --- a/http/handler.go +++ b/http/handler.go @@ -36,6 +36,7 @@ func NewApiRouter() (*Router, error) { p2p_handler := &p2pHandler{} lens_handler := &lensHandler{} ccip_handler := &ccipHandler{} + extras_handler := &extrasHandler{} router, err := NewRouter() if err != nil { @@ -47,6 +48,7 @@ func NewApiRouter() (*Router, error) { acp_handler.bindRoutes(router) p2p_handler.bindRoutes(router) ccip_handler.bindRoutes(router) + extras_handler.bindRoutes(router) router.AddRouteGroup(func(r *Router) { r.AddMiddleware(CollectionMiddleware) diff --git a/http/handler_extras.go b/http/handler_extras.go new file mode 100644 index 0000000000..c891e9befc --- /dev/null +++ b/http/handler_extras.go @@ -0,0 +1,47 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package http + +import ( + "net/http" + + "github.com/getkin/kin-openapi/openapi3" + + "github.com/sourcenetwork/defradb/client" + "github.com/sourcenetwork/defradb/event" +) + +// extrasHandler contains additional http handlers not found in client interfaces. +type extrasHandler struct{} + +func (s *extrasHandler) Purge(rw http.ResponseWriter, req *http.Request) { + db := req.Context().Value(dbContextKey).(client.DB) + rw.WriteHeader(http.StatusOK) // write the response before we restart to purge + db.Events().Publish(event.NewMessage(event.PurgeName, nil)) +} + +func (h *extrasHandler) bindRoutes(router *Router) { + errorResponse := &openapi3.ResponseRef{ + Ref: "#/components/responses/error", + } + successResponse := &openapi3.ResponseRef{ + Ref: "#/components/responses/success", + } + + purge := openapi3.NewOperation() + purge.Description = "Purge all persisted data and restart" + purge.OperationID = "purge" + purge.Responses = openapi3.NewResponses() + purge.Responses.Set("200", successResponse) + purge.Responses.Set("400", errorResponse) + + router.AddRoute("/purge", http.MethodPost, purge, h.Purge) +} diff --git a/http/handler_extras_test.go b/http/handler_extras_test.go new file mode 100644 index 0000000000..d7d1398e90 --- /dev/null +++ b/http/handler_extras_test.go @@ -0,0 +1,42 @@ +// Copyright 2024 Democratized Data Foundation +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package http + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/sourcenetwork/defradb/event" + + "github.com/stretchr/testify/require" +) + +func TestPurge(t *testing.T) { + cdb := setupDatabase(t) + url := "http://localhost:9181/api/v0/purge" + + req := httptest.NewRequest(http.MethodPost, url, nil) + rec := httptest.NewRecorder() + + purgeSub, err := cdb.Events().Subscribe(event.PurgeName) + require.NoError(t, err) + + handler, err := NewHandler(cdb) + require.NoError(t, err) + handler.ServeHTTP(rec, req) + + res := rec.Result() + require.Equal(t, 200, res.StatusCode) + + // test will timeout if purge never received + <-purgeSub.Message() +} diff --git a/node/errors.go b/node/errors.go index 84f0cdb006..2504f6ccb9 100644 --- a/node/errors.go +++ b/node/errors.go @@ -23,6 +23,7 @@ var ( ErrSignerMissingForSourceHubACP = errors.New("a txn signer must be provided for SourceHub ACP") ErrLensRuntimeNotSupported = errors.New(errLensRuntimeNotSupported) ErrStoreTypeNotSupported = errors.New(errStoreTypeNotSupported) + ErrPurgeWithDevModeDisabled = errors.New("cannot purge database when development mode is disabled") ) func NewErrLensRuntimeNotSupported(lens LensRuntimeType) error { diff --git a/node/node.go b/node/node.go index ffc4abd0ff..2603762684 100644 --- a/node/node.go +++ b/node/node.go @@ -39,8 +39,9 @@ type Option any // Options contains start configuration values. type Options struct { - disableP2P bool - disableAPI bool + disableP2P bool + disableAPI bool + enableDevelopment bool } // DefaultOptions returns options with default settings. @@ -65,103 +66,98 @@ func WithDisableAPI(disable bool) NodeOpt { } } +// WithEnableDevelopment sets the enable development mode flag. +func WithEnableDevelopment(enable bool) NodeOpt { + return func(o *Options) { + o.enableDevelopment = enable + } +} + // Node is a DefraDB instance with optional sub-systems. type Node struct { DB client.DB Peer *net.Peer Server *http.Server + + options *Options + dbOpts []db.Option + acpOpts []ACPOpt + netOpts []net.NodeOpt + storeOpts []StoreOpt + serverOpts []http.ServerOpt + lensOpts []LenOpt } -// NewNode returns a new node instance configured with the given options. -func NewNode(ctx context.Context, opts ...Option) (*Node, error) { - var ( - dbOpts []db.Option - acpOpts []ACPOpt - netOpts []net.NodeOpt - storeOpts []StoreOpt - serverOpts []http.ServerOpt - lensOpts []LenOpt - ) - - options := DefaultOptions() +// New returns a new node instance configured with the given options. +func New(ctx context.Context, opts ...Option) (*Node, error) { + n := Node{ + options: DefaultOptions(), + } for _, opt := range opts { switch t := opt.(type) { - case ACPOpt: - acpOpts = append(acpOpts, t) - case NodeOpt: - t(options) + t(n.options) + + case ACPOpt: + n.acpOpts = append(n.acpOpts, t) case StoreOpt: - storeOpts = append(storeOpts, t) + n.storeOpts = append(n.storeOpts, t) case db.Option: - dbOpts = append(dbOpts, t) + n.dbOpts = append(n.dbOpts, t) case http.ServerOpt: - serverOpts = append(serverOpts, t) + n.serverOpts = append(n.serverOpts, t) case net.NodeOpt: - netOpts = append(netOpts, t) + n.netOpts = append(n.netOpts, t) case LenOpt: - lensOpts = append(lensOpts, t) + n.lensOpts = append(n.lensOpts, t) } } + return &n, nil +} - rootstore, err := NewStore(ctx, storeOpts...) +// Start starts the node sub-systems. +func (n *Node) Start(ctx context.Context) error { + rootstore, err := NewStore(ctx, n.storeOpts...) if err != nil { - return nil, err + return err } - - acp, err := NewACP(ctx, acpOpts...) + acp, err := NewACP(ctx, n.acpOpts...) if err != nil { - return nil, err + return err } - - lens, err := NewLens(ctx, lensOpts...) + lens, err := NewLens(ctx, n.lensOpts...) if err != nil { - return nil, err + return err } - - db, err := db.NewDB(ctx, rootstore, acp, lens, dbOpts...) + n.DB, err = db.NewDB(ctx, rootstore, acp, lens, n.dbOpts...) if err != nil { - return nil, err + return err } - var peer *net.Peer - if !options.disableP2P { + if !n.options.disableP2P { // setup net node - peer, err = net.NewPeer(ctx, db.Blockstore(), db.Events(), netOpts...) + n.Peer, err = net.NewPeer(ctx, n.DB.Blockstore(), n.DB.Events(), n.netOpts...) if err != nil { - return nil, err + return err } } - var server *http.Server - if !options.disableAPI { + if !n.options.disableAPI { // setup http server - handler, err := http.NewHandler(db) + handler, err := http.NewHandler(n.DB) if err != nil { - return nil, err + return err } - server, err = http.NewServer(handler, serverOpts...) + n.Server, err = http.NewServer(handler, n.serverOpts...) if err != nil { - return nil, err + return err } - } - - return &Node{ - DB: db, - Peer: peer, - Server: server, - }, nil -} - -// Start starts the node sub-systems. -func (n *Node) Start(ctx context.Context) error { - if n.Server != nil { - err := n.Server.SetListener() + err = n.Server.SetListener() if err != nil { return err } @@ -174,6 +170,7 @@ func (n *Node) Start(ctx context.Context) error { } }() } + return nil } @@ -191,3 +188,20 @@ func (n *Node) Close(ctx context.Context) error { } return err } + +// PurgeAndRestart causes the node to shutdown, purge all data from +// its datastore, and restart. +func (n *Node) PurgeAndRestart(ctx context.Context) error { + if !n.options.enableDevelopment { + return ErrPurgeWithDevModeDisabled + } + err := n.Close(ctx) + if err != nil { + return err + } + err = purgeStore(ctx, n.storeOpts...) + if err != nil { + return err + } + return n.Start(ctx) +} diff --git a/node/node_test.go b/node/node_test.go index 1aa1dac92a..010f810da2 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -11,9 +11,13 @@ package node import ( + "context" "testing" + "github.com/sourcenetwork/defradb/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestWithDisableP2P(t *testing.T) { @@ -27,3 +31,56 @@ func TestWithDisableAPI(t *testing.T) { WithDisableAPI(true)(options) assert.Equal(t, true, options.disableAPI) } + +func TestWithEnableDevelopment(t *testing.T) { + options := &Options{} + WithEnableDevelopment(true)(options) + assert.Equal(t, true, options.enableDevelopment) +} + +func TestPurgeAndRestartWithDevModeDisabled(t *testing.T) { + ctx := context.Background() + + opts := []Option{ + WithDisableAPI(true), + WithDisableP2P(true), + WithStorePath(t.TempDir()), + } + + n, err := New(ctx, opts...) + require.NoError(t, err) + + err = n.Start(ctx) + require.NoError(t, err) + + err = n.PurgeAndRestart(ctx) + require.ErrorIs(t, err, ErrPurgeWithDevModeDisabled) +} + +func TestPurgeAndRestartWithDevModeEnabled(t *testing.T) { + ctx := context.Background() + + opts := []Option{ + WithDisableAPI(true), + WithDisableP2P(true), + WithStorePath(t.TempDir()), + WithEnableDevelopment(true), + } + + n, err := New(ctx, opts...) + require.NoError(t, err) + + err = n.Start(ctx) + require.NoError(t, err) + + _, err = n.DB.AddSchema(ctx, "type User { name: String }") + require.NoError(t, err) + + err = n.PurgeAndRestart(ctx) + require.NoError(t, err) + + schemas, err := n.DB.GetSchemas(ctx, client.SchemaFetchOptions{}) + require.NoError(t, err) + + assert.Len(t, schemas, 0) +} diff --git a/node/store.go b/node/store.go index 373610b0e1..4a0e8d93da 100644 --- a/node/store.go +++ b/node/store.go @@ -31,6 +31,12 @@ const ( // allows it's population to be managed by build flags. var storeConstructors = map[StoreType]func(ctx context.Context, options *StoreOptions) (datastore.Rootstore, error){} +// storePurgeFuncs is a map of [StoreType]s to store purge functions. +// +// Is is populated by the `init` functions in the runtime-specific files - this +// allows it's population to be managed by build flags. +var storePurgeFuncs = map[StoreType]func(ctx context.Context, options *StoreOptions) error{} + // StoreOptions contains store configuration values. type StoreOptions struct { store StoreType @@ -77,3 +83,15 @@ func NewStore(ctx context.Context, opts ...StoreOpt) (datastore.Rootstore, error } return nil, NewErrStoreTypeNotSupported(options.store) } + +func purgeStore(ctx context.Context, opts ...StoreOpt) error { + options := DefaultStoreOptions() + for _, opt := range opts { + opt(options) + } + purgeFunc, ok := storePurgeFuncs[options.store] + if ok { + return purgeFunc(ctx, options) + } + return NewErrStoreTypeNotSupported(options.store) +} diff --git a/node/store_badger.go b/node/store_badger.go index 5c252d3607..94ee2c3dbf 100644 --- a/node/store_badger.go +++ b/node/store_badger.go @@ -38,8 +38,23 @@ func init() { return badger.NewDatastore(options.path, &badgerOpts) } + purge := func(ctx context.Context, options *StoreOptions) error { + store, err := constructor(ctx, options) + if err != nil { + return err + } + err = store.(*badger.Datastore).DB.DropAll() + if err != nil { + return err + } + return store.Close() + } + storeConstructors[BadgerStore] = constructor + storePurgeFuncs[BadgerStore] = purge + storeConstructors[DefaultStore] = constructor + storePurgeFuncs[DefaultStore] = purge } // WithBadgerInMemory sets the badger in memory option. diff --git a/node/store_memory.go b/node/store_memory.go index 352381fa9d..84911d5da8 100644 --- a/node/store_memory.go +++ b/node/store_memory.go @@ -24,9 +24,14 @@ func init() { constructor := func(ctx context.Context, options *StoreOptions) (datastore.Rootstore, error) { return memory.NewDatastore(ctx), nil } + purge := func(ctx context.Context, options *StoreOptions) error { + return nil + } // don't override the default constructor if previously set if _, ok := storeConstructors[DefaultStore]; !ok { storeConstructors[DefaultStore] = constructor + storePurgeFuncs[DefaultStore] = purge } storeConstructors[MemoryStore] = constructor + storePurgeFuncs[MemoryStore] = purge } diff --git a/tests/gen/schema_parser.go b/tests/gen/schema_parser.go index 3e08212b5c..ebf3a813ea 100644 --- a/tests/gen/schema_parser.go +++ b/tests/gen/schema_parser.go @@ -26,7 +26,7 @@ func ParseSDL(gqlSDL string) (map[string]client.CollectionDefinition, error) { // Spinning up a temporary in-memory node with all extras disabled is the // most reliable and cheapest maintainance-cost-wise way to fully parse // the SDL and correctly link all relations. - node, err := node.NewNode( + node, err := node.New( ctx, node.WithBadgerInMemory(true), node.WithDisableAPI(true), diff --git a/tests/integration/db.go b/tests/integration/db.go index 54175784c0..06737318d7 100644 --- a/tests/integration/db.go +++ b/tests/integration/db.go @@ -73,14 +73,19 @@ func init() { func NewBadgerMemoryDB(ctx context.Context) (client.DB, error) { opts := []node.Option{ + node.WithDisableP2P(true), + node.WithDisableAPI(true), node.WithBadgerInMemory(true), } - node, err := node.NewNode(ctx, opts...) + node, err := node.New(ctx, opts...) + if err != nil { + return nil, err + } + err = node.Start(ctx) if err != nil { return nil, err } - return node.DB, err } @@ -88,14 +93,19 @@ func NewBadgerFileDB(ctx context.Context, t testing.TB) (client.DB, error) { path := t.TempDir() opts := []node.Option{ + node.WithDisableP2P(true), + node.WithDisableAPI(true), node.WithStorePath(path), } - node, err := node.NewNode(ctx, opts...) + node, err := node.New(ctx, opts...) + if err != nil { + return nil, err + } + err = node.Start(ctx) if err != nil { return nil, err } - return node.DB, err } @@ -175,10 +185,13 @@ func setupNode(s *state) (*node.Node, string, error) { return nil, "", fmt.Errorf("invalid database type: %v", s.dbt) } - node, err := node.NewNode(s.ctx, opts...) + node, err := node.New(s.ctx, opts...) + if err != nil { + return nil, "", err + } + err = node.Start(s.ctx) if err != nil { return nil, "", err } - return node, path, nil }