diff --git a/Makefile b/Makefile index 2e43a5b..e37a9cf 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ test: go test -cover ./... run: - go run $(FLAGS) ./cmd/inceptiondb/... + STATICS=statics/www/ go run $(FLAGS) ./cmd/inceptiondb/... build: go build $(FLAGS) -o bin/ ./cmd/inceptiondb/... diff --git a/api/apicollectionv1/0_build.go b/api/apicollectionv1/0_build.go index c141fe5..5441040 100644 --- a/api/apicollectionv1/0_build.go +++ b/api/apicollectionv1/0_build.go @@ -30,6 +30,7 @@ func BuildV1Collection(v1 *box.R, s service.Servicer) *box.R { box.ActionPost(dropIndex), box.ActionPost(getIndex), box.ActionPost(size), + box.ActionPost(setDefaults), ) return collections diff --git a/api/apicollectionv1/collection.go b/api/apicollectionv1/collection.go index c2a4cce..0017f4a 100644 --- a/api/apicollectionv1/collection.go +++ b/api/apicollectionv1/collection.go @@ -1,7 +1,8 @@ package apicollectionv1 type CollectionResponse struct { - Name string `json:"name"` - Total int `json:"total"` - Indexes int `json:"indexes"` + Name string `json:"name"` + Total int `json:"total"` + Indexes int `json:"indexes"` + Defaults map[string]any `json:"defaults"` } diff --git a/api/apicollectionv1/createCollection.go b/api/apicollectionv1/createCollection.go index 7fe5191..72e4a47 100644 --- a/api/apicollectionv1/createCollection.go +++ b/api/apicollectionv1/createCollection.go @@ -8,7 +8,14 @@ import ( ) type createCollectionRequest struct { - Name string `json:"name"` + Name string `json:"name"` + Defaults map[string]any `json:"defaults"` +} + +func newCollectionDefaults() map[string]any { + return map[string]any{ + "id": "uuid()", + } } func createCollection(ctx context.Context, w http.ResponseWriter, input *createCollectionRequest) (*CollectionResponse, error) { @@ -25,9 +32,15 @@ func createCollection(ctx context.Context, w http.ResponseWriter, input *createC return nil, err // todo: wrap error? } + if input.Defaults == nil { + input.Defaults = newCollectionDefaults() + } + collection.SetDefaults(input.Defaults) + w.WriteHeader(http.StatusCreated) return &CollectionResponse{ - Name: input.Name, - Total: len(collection.Rows), + Name: input.Name, + Total: len(collection.Rows), + Defaults: collection.Defaults, }, nil } diff --git a/api/apicollectionv1/createIndex.go b/api/apicollectionv1/createIndex.go index d5f6926..a15a04b 100644 --- a/api/apicollectionv1/createIndex.go +++ b/api/apicollectionv1/createIndex.go @@ -43,6 +43,13 @@ func createIndex(ctx context.Context, r *http.Request) (*listIndexesItem, error) col, err := s.GetCollection(collectionName) if err == service.ErrorCollectionNotFound { col, err = s.CreateCollection(collectionName) + if err != nil { + return nil, err // todo: handle/wrap this properly + } + err = col.SetDefaults(newCollectionDefaults()) + if err != nil { + return nil, err // todo: handle/wrap this properly + } } if err != nil { return nil, err // todo: handle/wrap this properly diff --git a/api/apicollectionv1/dropIndex.go b/api/apicollectionv1/dropIndex.go index 30c0b1f..f7348cc 100644 --- a/api/apicollectionv1/dropIndex.go +++ b/api/apicollectionv1/dropIndex.go @@ -20,6 +20,13 @@ func dropIndex(ctx context.Context, w http.ResponseWriter, input *dropIndexReque col, err := s.GetCollection(collectionName) if err == service.ErrorCollectionNotFound { col, err = s.CreateCollection(collectionName) + if err != nil { + return err // todo: handle/wrap this properly + } + err = col.SetDefaults(newCollectionDefaults()) + if err != nil { + return err // todo: handle/wrap this properly + } } if err != nil { return err // todo: handle/wrap this properly diff --git a/api/apicollectionv1/getCollection.go b/api/apicollectionv1/getCollection.go index 1e61098..a90c9b7 100644 --- a/api/apicollectionv1/getCollection.go +++ b/api/apicollectionv1/getCollection.go @@ -23,8 +23,9 @@ func getCollection(ctx context.Context) (*CollectionResponse, error) { } return &CollectionResponse{ - Name: collectionName, - Total: len(collection.Rows), - Indexes: len(collection.Indexes), + Name: collectionName, + Total: len(collection.Rows), + Indexes: len(collection.Indexes), + Defaults: collection.Defaults, }, nil } diff --git a/api/apicollectionv1/insert.go b/api/apicollectionv1/insert.go index 7970b4c..0e81fa5 100644 --- a/api/apicollectionv1/insert.go +++ b/api/apicollectionv1/insert.go @@ -19,34 +19,52 @@ func insert(ctx context.Context, w http.ResponseWriter, r *http.Request) error { collection, err := s.GetCollection(collectionName) if err == service.ErrorCollectionNotFound { collection, err = s.CreateCollection(collectionName) + if err != nil { + return err // todo: handle/wrap this properly + } + err = collection.SetDefaults(newCollectionDefaults()) + if err != nil { + return err // todo: handle/wrap this properly + } } if err != nil { return err // todo: handle/wrap this properly } jsonReader := json.NewDecoder(r.Body) + jsonWriter := json.NewEncoder(w) - for { - item := map[string]interface{}{} + for i := 0; true; i++ { + item := map[string]any{} err := jsonReader.Decode(&item) if err == io.EOF { - w.WriteHeader(http.StatusCreated) + if i == 0 { + w.WriteHeader(http.StatusNoContent) + } return nil } if err != nil { // TODO: handle error properly fmt.Println("ERROR:", err.Error()) - w.WriteHeader(http.StatusBadRequest) + if i == 0 { + w.WriteHeader(http.StatusBadRequest) + } return err } - _, err = collection.Insert(item) + row, err := collection.Insert(item) if err != nil { // TODO: handle error properly - w.WriteHeader(http.StatusConflict) + if i == 0 { + w.WriteHeader(http.StatusConflict) + } return err } - // jsonWriter.Encode(item) + if i == 0 { + w.WriteHeader(http.StatusCreated) + } + jsonWriter.Encode(row.Payload) } + return nil } diff --git a/api/apicollectionv1/listCollections.go b/api/apicollectionv1/listCollections.go index 2bea991..41eb10f 100644 --- a/api/apicollectionv1/listCollections.go +++ b/api/apicollectionv1/listCollections.go @@ -12,9 +12,10 @@ func listCollections(ctx context.Context, w http.ResponseWriter) ([]*CollectionR response := []*CollectionResponse{} for name, collection := range s.ListCollections() { response = append(response, &CollectionResponse{ - Name: name, - Total: len(collection.Rows), - Indexes: len(collection.Indexes), + Name: name, + Total: len(collection.Rows), + Indexes: len(collection.Indexes), + Defaults: collection.Defaults, }) } return response, nil diff --git a/api/apicollectionv1/setDefaults.go b/api/apicollectionv1/setDefaults.go new file mode 100644 index 0000000..8442991 --- /dev/null +++ b/api/apicollectionv1/setDefaults.go @@ -0,0 +1,62 @@ +package apicollectionv1 + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/fulldump/box" + + "github.com/fulldump/inceptiondb/service" +) + +type setDefaultsInput map[string]any + +func setDefaults(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + + s := GetServicer(ctx) + collectionName := box.GetUrlParameter(ctx, "collectionName") + col, err := s.GetCollection(collectionName) + if err == service.ErrorCollectionNotFound { + col, err = s.CreateCollection(collectionName) + if err != nil { + return err // todo: handle/wrap this properly + } + err = col.SetDefaults(newCollectionDefaults()) + if err != nil { + return err // todo: handle/wrap this properly + } + } + if err != nil { + return err // todo: handle/wrap this properly + } + + defaults := col.Defaults + + err = json.NewDecoder(r.Body).Decode(&defaults) + if err != nil { + return err // todo: handle/wrap this properly + } + + for k, v := range defaults { + if v == nil { + delete(defaults, k) + } + } + + if len(defaults) == 0 { + defaults = nil + } + + err = col.SetDefaults(defaults) + if err != nil { + return err + } + + err = json.NewEncoder(w).Encode(col.Defaults) + if err != nil { + return err // todo: handle/wrap this properly + } + + return nil +} diff --git a/collection/collection.go b/collection/collection.go index 00fe550..247e5fc 100644 --- a/collection/collection.go +++ b/collection/collection.go @@ -6,6 +6,7 @@ import ( "io" "os" "sync" + "sync/atomic" "time" jsonpatch "github.com/evanphx/json-patch" @@ -21,6 +22,8 @@ type Collection struct { rowsMutex *sync.Mutex Indexes map[string]*collectionIndex // todo: protect access with mutex or use sync.Map // buffer *bufio.Writer // TODO: use write buffer to improve performance (x3 in tests) + Defaults map[string]any + count int64 } type collectionIndex struct { @@ -118,6 +121,10 @@ func OpenCollection(filename string) (*Collection, error) { if err != nil { fmt.Printf("WARNING: patch item %d: %s\n", params.I, err.Error()) } + case "set_defaults": + defaults := map[string]any{} + json.Unmarshal(command.Payload, &defaults) + collection.setDefaults(defaults, false) } } @@ -161,6 +168,33 @@ func (c *Collection) Insert(item interface{}) (*Row, error) { return nil, fmt.Errorf("json encode payload: %w", err) } + auto := atomic.AddInt64(&c.count, 1) + + if c.Defaults != nil { + item := map[string]any{} // todo: item is shadowed, choose a better name + for k, v := range c.Defaults { + switch v { + case "uuid()": + item[k] = uuid.NewString() + case "unixnano()": + item[k] = time.Now().UnixNano() + case "auto()": + item[k] = auto + default: + item[k] = v + } + } + err := json.Unmarshal(payload, &item) + if err != nil { + return nil, fmt.Errorf("json encode defaults: %w", err) + } + + payload, err = json.Marshal(item) + if err != nil { + return nil, fmt.Errorf("json encode payload: %w", err) + } + } + // Add row row, err := c.addRow(payload) if err != nil { @@ -222,6 +256,39 @@ type CreateIndexCommand struct { Options interface{} `json:"options"` } +func (c *Collection) SetDefaults(defaults map[string]any) error { + return c.setDefaults(defaults, true) +} + +func (c *Collection) setDefaults(defaults map[string]any, persist bool) error { + + c.Defaults = defaults + + if !persist { + return nil + } + + payload, err := json.Marshal(defaults) + if err != nil { + return fmt.Errorf("json encode payload: %w", err) + } + + command := &Command{ + Name: "set_defaults", // todo: rename to create_index + Uuid: uuid.New().String(), + Timestamp: time.Now().UnixNano(), + StartByte: 0, + Payload: payload, + } + + err = json.NewEncoder(c.file).Encode(command) + if err != nil { + return fmt.Errorf("json encode command: %w", err) + } + + return nil +} + // IndexMap create a unique index with a name // Constraints: values can be only scalar strings or array of strings func (c *Collection) Index(name string, options interface{}) error { // todo: rename to CreateIndex diff --git a/doc/examples/create_collection.md b/doc/examples/create_collection.md index 2ecc4cd..9c69335 100644 --- a/doc/examples/create_collection.md +++ b/doc/examples/create_collection.md @@ -21,11 +21,14 @@ Host: example.com } HTTP/1.1 201 Created -Content-Length: 47 +Content-Length: 74 Content-Type: text/plain; charset=utf-8 Date: Mon, 15 Aug 2022 02:08:13 GMT { + "defaults": { + "id": "uuid()" + }, "indexes": 0, "name": "my-collection", "total": 0 diff --git a/doc/examples/insert_many.md b/doc/examples/insert_many.md index f2a023a..b4b022d 100644 --- a/doc/examples/insert_many.md +++ b/doc/examples/insert_many.md @@ -23,9 +23,13 @@ Host: example.com HTTP/1.1 201 Created -Content-Length: 0 +Content-Length: 84 +Content-Type: text/plain; charset=utf-8 Date: Mon, 15 Aug 2022 02:08:13 GMT +{"id":"1","name":"Alfonso"} +{"id":"2","name":"Gerardo"} +{"id":"3","name":"Alfonso"} ``` diff --git a/doc/examples/insert_one.md b/doc/examples/insert_one.md index ed2c262..3f2b2aa 100644 --- a/doc/examples/insert_one.md +++ b/doc/examples/insert_one.md @@ -25,10 +25,15 @@ Host: example.com } HTTP/1.1 201 Created -Content-Length: 0 +Content-Length: 58 +Content-Type: text/plain; charset=utf-8 Date: Mon, 15 Aug 2022 02:08:13 GMT - +{ + "address": "Elm Street 11", + "id": "my-id", + "name": "Fulanez" +} ``` diff --git a/doc/examples/list_collections.md b/doc/examples/list_collections.md index f660497..5a6414e 100644 --- a/doc/examples/list_collections.md +++ b/doc/examples/list_collections.md @@ -16,12 +16,15 @@ Host: example.com HTTP/1.1 200 OK -Content-Length: 49 +Content-Length: 76 Content-Type: text/plain; charset=utf-8 Date: Mon, 15 Aug 2022 02:08:13 GMT [ { + "defaults": { + "id": "uuid()" + }, "indexes": 0, "name": "my-collection", "total": 0 diff --git a/doc/examples/retrieve_collection.md b/doc/examples/retrieve_collection.md index f098d70..0059d1e 100644 --- a/doc/examples/retrieve_collection.md +++ b/doc/examples/retrieve_collection.md @@ -16,11 +16,14 @@ Host: example.com HTTP/1.1 200 OK -Content-Length: 47 +Content-Length: 74 Content-Type: text/plain; charset=utf-8 Date: Mon, 15 Aug 2022 02:08:13 GMT { + "defaults": { + "id": "uuid()" + }, "indexes": 0, "name": "my-collection", "total": 0 diff --git a/doc/examples/set_defaults.md b/doc/examples/set_defaults.md new file mode 100644 index 0000000..2815e94 --- /dev/null +++ b/doc/examples/set_defaults.md @@ -0,0 +1,170 @@ +# Set defaults + + +The `SetDefaults` function is designed to automatically assign predefined default values to specific +fields in a document when a new entry is added to a database collection. This ensures consistency and +completeness of data, especially for fields that require a default state or value. + +## Overview + +When you insert a new document into the collection, `SetDefaults` intervenes by checking for any fields +that have not been explicitly provided in the input document. For such fields, if default values have +been predefined using SetDefaults, those values are automatically added to the document before it is +inserted into the collection. This process is seamless and ensures that every new document adheres to +a defined structure and contains all necessary information. + +## Example usage + +Consider a scenario where you are adding a new user record to a collection but only provide the user's +name. If `SetDefaults` has been configured for the collection, it will automatically fill in any missing +fields that have default values defined. + +### Input Document + +When you attempt to insert a document with just the user's name: + +```json +{ + "name": "Fulanez" +} +``` + +### Predefined Defaults + +Assume the following default values have been set for the collection: + +```json +{ + "id": "uuid()", // A function generating a unique identifier + "verified": false // A boolean flag set to false by default +} +``` + +### Resulting Document + +With `SetDefaults` applied, the document that gets inserted into the collection will include the missing +fields with their default values: + +```json +{ + "id": "3bb5afae-c7b7-11ee-86b0-4f000ceb9a36", // Generated unique ID + "name": "Fulanez", // Provided by the user + "verified": false // Default value +} +``` + +## Benefits + +* **Consistency**: Ensures that all documents in the collection follow a consistent structure, even when +some data points are not provided during insertion. +* **Completeness**: Guarantees that essential fields are always populated, either by the user or through +default values, ensuring data integrity. +* **Efficiency**: Saves time and effort by automating the assignment of common default values, reducing +the need for manual data entry or post-insertion updates. + +## Configuration + +To utilize `SetDefaults`, you must first define the default values for the desired fields in your +collection's configuration. This typically involves specifying a field name and its corresponding +default value or function (e.g., uuid() for generating unique identifiers). + +It's important to note that `SetDefaults` only applies to new documents being inserted into the +collection. It does not affect documents that are already present in the collection or those being +updated. + +## Generative Functions in `SetDefaults` + +`SetDefaults` supports a variety of generative functions to automatically assign dynamic values to +fields in new documents. These functions are executed at the time of document insertion, ensuring that +each entry receives a unique or contextually appropriate value based on the specified function. Below is +a list of supported generative functions: + +### 1. `uuid()` + +**Description**: Generates a Universally Unique Identifier (UUID) for the document. This is particularly +useful for assigning a unique identifier to each entry, ensuring that each document can be distinctly +identified within the collection. + +**Example Usage**: Ideal for fields requiring a unique ID, such as user identifiers, transaction IDs, etc. + +**Output Example**: `"id": "3bb5afae-c7b7-11ee-86b0-4f000ceb9a36"` + +### 2. `unixnano()` +**Description**: Produces a numerical value representing the current time in Unix nanoseconds. This +function is handy for timestamping documents at the exact time of their creation, providing +high-resolution time tracking. + +**Example Usage**: Suitable for fields that need to record the precise time of document insertion, +like creation timestamps, log entries, etc. + +**Output Example**: `"created_at": 16180339887467395` (represents the number of nanoseconds since +January 1, 1970, 00:00:00 UTC) + +### 3. `auto()` +**Description**: Implements an automatic row counter that increments with each insert, starting from +the first insertion. This function is beneficial for maintaining a sequential order or count of the +documents added to the collection. + +**Example Usage**: Useful for auto-increment fields, such as a serial number, order number, or any +scenario where a simple, incrementing counter is needed. + +**Output Example**: `"serial_number": 1023` (where 1023 is the current count of documents inserted +since the first one) + +### Implementation Considerations + +When integrating generative functions with `SetDefaults`, consider the following: + +**Uniqueness**: Functions like uuid() guarantee uniqueness, making them ideal for identifiers. + +**Temporal Precision**: unixnano() provides high-precision timestamps, useful for time-sensitive data. + +**Sequential Integrity**: auto() ensures a consistent, incremental sequence, beneficial for ordering or +numbering entries. + +Ensure that the chosen generative function aligns with the field's purpose and the overall data model's +requirements. Proper configuration of `SetDefaults` with these functions enhances data integrity, +consistency, and utility within your application. + + +Curl example: + +```sh +curl -X POST "https://example.com/v1/collections/my-collection:setDefaults" \ +-d '{ + "created_on": "unixnano()", + "name": "", + "street": "", + "verified": false +}' +``` + + +HTTP request/response example: + +```http +POST /v1/collections/my-collection:setDefaults HTTP/1.1 +Host: example.com + +{ + "created_on": "unixnano()", + "name": "", + "street": "", + "verified": false +} + +HTTP/1.1 200 OK +Content-Length: 81 +Content-Type: text/plain; charset=utf-8 +Date: Mon, 15 Aug 2022 02:08:13 GMT + +{ + "created_on": "unixnano()", + "id": "uuid()", + "name": "", + "street": "", + "verified": false +} +``` + + diff --git a/doc/examples/size_-_experimental.md b/doc/examples/size_-_experimental.md index eb4ceb5..e089b6d 100644 --- a/doc/examples/size_-_experimental.md +++ b/doc/examples/size_-_experimental.md @@ -25,7 +25,7 @@ Content-Type: text/plain; charset=utf-8 Date: Mon, 15 Aug 2022 02:08:13 GMT { - "disk": 640, + "disk": 783, "index.my-index": 192, "memory": 248 } diff --git a/service/0_save.go b/service/0_save.go index 1ceef5c..f13997e 100644 --- a/service/0_save.go +++ b/service/0_save.go @@ -133,7 +133,7 @@ func writeFile(filename, text string) { func md_description(d string) string { d = md_crop_tabs(d) d = strings.Replace(d, "\n´´´", "\n```", -1) - // d = strings.Replace(d, "´", "`", -1) + d = strings.Replace(d, "´", "`", -1) return d } diff --git a/service/acceptance.go b/service/acceptance.go index 3ff6492..c8f71c0 100644 --- a/service/acceptance.go +++ b/service/acceptance.go @@ -25,11 +25,15 @@ func Acceptance(a *biff.A, apiRequest func(method, path string) *apitest.Request biff.AssertEqual(resp.StatusCode, http.StatusCreated) expectedBody := JSON{ - "name": "my-collection", - "total": 0, - "indexes": 0, + "name": "my-collection", + "total": 0, + "indexes": 0, + "defaults": map[string]any{"id": "uuid()"}, + } + zzz := biff.AssertEqualJson(resp.BodyJson(), expectedBody) + if !zzz { + fmt.Println("JODERRRRRRR") } - biff.AssertEqualJson(resp.BodyJson(), expectedBody) a.Alternative("Retrieve collection", func(a *biff.A) { resp := apiRequest("GET", "/collections/my-collection").Do() @@ -37,9 +41,10 @@ func Acceptance(a *biff.A, apiRequest func(method, path string) *apitest.Request biff.AssertEqual(resp.StatusCode, http.StatusOK) expectedBody := JSON{ - "name": "my-collection", - "total": 0, - "indexes": 0, + "name": "my-collection", + "total": 0, + "indexes": 0, + "defaults": map[string]any{"id": "uuid()"}, } biff.AssertEqualJson(resp.BodyJson(), expectedBody) }) @@ -51,9 +56,10 @@ func Acceptance(a *biff.A, apiRequest func(method, path string) *apitest.Request biff.AssertEqual(resp.StatusCode, http.StatusOK) expectedBody := []JSON{ { - "name": "my-collection", - "total": 0, - "indexes": 0, + "name": "my-collection", + "total": 0, + "indexes": 0, + "defaults": map[string]any{"id": "uuid()"}, }, } biff.AssertEqualJson(resp.BodyJson(), expectedBody) @@ -86,7 +92,13 @@ func Acceptance(a *biff.A, apiRequest func(method, path string) *apitest.Request Save(resp, "Insert one", ``) biff.AssertEqual(resp.StatusCode, http.StatusCreated) - biff.AssertEqual(resp.BodyString(), "") + + expectedBody := map[string]any{ + "address": "Elm Street 11", + "id": "my-id", + "name": "Fulanez", + } + biff.AssertEqual(resp.BodyJson(), expectedBody) a.Alternative("Find with fullscan", func(a *biff.A) { resp := apiRequest("POST", "/collections/my-collection:find"). @@ -615,6 +627,230 @@ func Acceptance(a *biff.A, apiRequest func(method, path string) *apitest.Request biff.AssertEqual(resp.StatusCode, http.StatusInternalServerError) // todo: it should return 404 }) + a.Alternative("Set defaults", func(a *biff.A) { + resp := apiRequest("POST", "/collections/my-collection:setDefaults"). + WithBodyJson(JSON{ + "c": "auto()", + "n": "auto()", + "name": "", + "street": "", + "verified": false, + }).Do() + + expectedBody := JSON{ + "id": "uuid()", + "c": "auto()", + "n": "auto()", + "name": "", + "street": "", + "verified": false, + } + biff.AssertEqualJson(resp.BodyJson(), expectedBody) + + a.Alternative("Insert with defaults - overwrite field", func(a *biff.A) { + resp := apiRequest("POST", "/collections/my-collection:insert"). + WithBodyJson(JSON{ + "name": "fulanez", + }).Do() + expectedBody := JSON{ + "id": resp.BodyJson().(JSON)["id"], + "c": 1, + "n": 1, + "name": "fulanez", + "street": "", + "verified": false, + } + biff.AssertEqualJson(resp.BodyJson(), expectedBody) + }) + + a.Alternative("Insert with defaults - new field", func(a *biff.A) { + resp := apiRequest("POST", "/collections/my-collection:insert"). + WithBodyJson(JSON{ + "new": "field", + }).Do() + expectedBody := JSON{ + "id": resp.BodyJson().(JSON)["id"], + "c": 1, + "n": 1, + "name": "", + "street": "", + "verified": false, + "new": "field", + } + biff.AssertEqualJson(resp.BodyJson(), expectedBody) + }) + + }) + + a.Alternative("Set defaults - example", func(a *biff.A) { + resp := apiRequest("POST", "/collections/my-collection:setDefaults"). + WithBodyJson(JSON{ + "created_on": "unixnano()", + "name": "", + "street": "", + "verified": false, + }).Do() + + Save(resp, "Set defaults", ` + + The ´SetDefaults´ function is designed to automatically assign predefined default values to specific + fields in a document when a new entry is added to a database collection. This ensures consistency and + completeness of data, especially for fields that require a default state or value. + + ## Overview + + When you insert a new document into the collection, ´SetDefaults´ intervenes by checking for any fields + that have not been explicitly provided in the input document. For such fields, if default values have + been predefined using SetDefaults, those values are automatically added to the document before it is + inserted into the collection. This process is seamless and ensures that every new document adheres to + a defined structure and contains all necessary information. + + ## Example usage + + Consider a scenario where you are adding a new user record to a collection but only provide the user's + name. If ´SetDefaults´ has been configured for the collection, it will automatically fill in any missing + fields that have default values defined. + + ### Input Document + + When you attempt to insert a document with just the user's name: + + ´´´json + { + "name": "Fulanez" + } + ´´´ + + ### Predefined Defaults + + Assume the following default values have been set for the collection: + + ´´´json + { + "id": "uuid()", // A function generating a unique identifier + "verified": false // A boolean flag set to false by default + } + ´´´ + + ### Resulting Document + + With ´SetDefaults´ applied, the document that gets inserted into the collection will include the missing + fields with their default values: + + ´´´json + { + "id": "3bb5afae-c7b7-11ee-86b0-4f000ceb9a36", // Generated unique ID + "name": "Fulanez", // Provided by the user + "verified": false // Default value + } + ´´´ + + ## Benefits + + * **Consistency**: Ensures that all documents in the collection follow a consistent structure, even when + some data points are not provided during insertion. + * **Completeness**: Guarantees that essential fields are always populated, either by the user or through + default values, ensuring data integrity. + * **Efficiency**: Saves time and effort by automating the assignment of common default values, reducing + the need for manual data entry or post-insertion updates. + + ## Configuration + + To utilize ´SetDefaults´, you must first define the default values for the desired fields in your + collection's configuration. This typically involves specifying a field name and its corresponding + default value or function (e.g., uuid() for generating unique identifiers). + + It's important to note that ´SetDefaults´ only applies to new documents being inserted into the + collection. It does not affect documents that are already present in the collection or those being + updated. + + ## Generative Functions in ´SetDefaults´ + + ´SetDefaults´ supports a variety of generative functions to automatically assign dynamic values to + fields in new documents. These functions are executed at the time of document insertion, ensuring that + each entry receives a unique or contextually appropriate value based on the specified function. Below is + a list of supported generative functions: + + ### 1. ´uuid()´ + + **Description**: Generates a Universally Unique Identifier (UUID) for the document. This is particularly + useful for assigning a unique identifier to each entry, ensuring that each document can be distinctly + identified within the collection. + + **Example Usage**: Ideal for fields requiring a unique ID, such as user identifiers, transaction IDs, etc. + + **Output Example**: ´"id": "3bb5afae-c7b7-11ee-86b0-4f000ceb9a36"´ + + ### 2. ´unixnano()´ + **Description**: Produces a numerical value representing the current time in Unix nanoseconds. This + function is handy for timestamping documents at the exact time of their creation, providing + high-resolution time tracking. + + **Example Usage**: Suitable for fields that need to record the precise time of document insertion, + like creation timestamps, log entries, etc. + + **Output Example**: ´"created_at": 16180339887467395´ (represents the number of nanoseconds since + January 1, 1970, 00:00:00 UTC) + + ### 3. ´auto()´ + **Description**: Implements an automatic row counter that increments with each insert, starting from + the first insertion. This function is beneficial for maintaining a sequential order or count of the + documents added to the collection. + + **Example Usage**: Useful for auto-increment fields, such as a serial number, order number, or any + scenario where a simple, incrementing counter is needed. + + **Output Example**: ´"serial_number": 1023´ (where 1023 is the current count of documents inserted + since the first one) + + ### Implementation Considerations + + When integrating generative functions with ´SetDefaults´, consider the following: + + **Uniqueness**: Functions like uuid() guarantee uniqueness, making them ideal for identifiers. + + **Temporal Precision**: unixnano() provides high-precision timestamps, useful for time-sensitive data. + + **Sequential Integrity**: auto() ensures a consistent, incremental sequence, beneficial for ordering or + numbering entries. + + Ensure that the chosen generative function aligns with the field's purpose and the overall data model's + requirements. Proper configuration of ´SetDefaults´ with these functions enhances data integrity, + consistency, and utility within your application. + + `) + + expectedBody := JSON{ + "id": "uuid()", + "created_on": "unixnano()", + "name": "", + "street": "", + "verified": false, + } + biff.AssertEqualJson(resp.BodyJson(), expectedBody) + }) + + a.Alternative("Set defaults - auto", func(a *biff.A) { + apiRequest("POST", "/collections/my-collection:setDefaults"). + WithBodyJson(JSON{ + "id": nil, + "n": "auto()", + }).Do() + + a.Alternative("Insert multiple", func(a *biff.A) { + for i := 1; i <= 4; i++ { + resp := apiRequest("POST", "/collections/my-collection:insert"). + WithBodyJson(JSON{}).Do() + + expectedBody := JSON{ + "n": i, + } + biff.AssertEqualJson(resp.BodyJson(), expectedBody) + } + }) + + }) + }) a.Alternative("Insert on not existing collection", func(a *biff.A) { @@ -625,7 +861,10 @@ func Acceptance(a *biff.A, apiRequest func(method, path string) *apitest.Request resp := apiRequest("POST", "/collections/my-collection:insert"). WithBodyJson(myDocument).Do() - biff.AssertEqual(resp.BodyString(), "") + expectedBody := map[string]any{ + "id": "my-id", + } + biff.AssertEqual(resp.BodyJson(), expectedBody) biff.AssertEqual(resp.StatusCode, http.StatusCreated) a.Alternative("List collection", func(a *biff.A) { diff --git a/statics/www/index.html b/statics/www/index.html index 280054e..939c545 100644 --- a/statics/www/index.html +++ b/statics/www/index.html @@ -800,13 +800,20 @@