diff --git a/crud/data-preparation/users.json b/crud/.data-preparation/users.json
similarity index 100%
rename from crud/data-preparation/users.json
rename to crud/.data-preparation/users.json
diff --git a/crud/data-preparation/users_import.sql b/crud/.data-preparation/users_import.sql
similarity index 100%
rename from crud/data-preparation/users_import.sql
rename to crud/.data-preparation/users_import.sql
diff --git a/crud/.server/Makefile b/crud/.server/Makefile
new file mode 100644
index 0000000..382134c
--- /dev/null
+++ b/crud/.server/Makefile
@@ -0,0 +1,5 @@
+run:
+ go run .
+
+build:
+ go build -tags prod .
\ No newline at end of file
diff --git a/crud/.server/api/echo.go b/crud/.server/api/echo.go
new file mode 100644
index 0000000..e45fe02
--- /dev/null
+++ b/crud/.server/api/echo.go
@@ -0,0 +1,98 @@
+package api
+
+import (
+ "encoding/json"
+ "log"
+ "mime"
+ "net/http"
+ "net/url"
+ "strings"
+
+)
+
+// isJSONLike checks if a string looks like it might be JSON
+func isJSONLike(s string) bool {
+ s = strings.TrimSpace(s)
+ return (strings.HasPrefix(s, "{") && strings.HasSuffix(s, "}")) ||
+ (strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]"))
+}
+
+// RequestEcho represents the structure of our response
+type RequestEcho struct {
+ URL string `json:"url"`
+ Method string `json:"method"`
+ Path string `json:"path"`
+ QueryParams url.Values `json:"query_params"`
+ Headers http.Header `json:"headers"` // Use http.Header for canonical keys
+ Body interface{} `json:"body"` // Can be structured JSON or raw string
+ BodyRaw string `json:"body_raw"` // Original body as string
+ RemoteAddr string `json:"remote_addr"`
+ ContentType string `json:"content_type"`
+ ContentLength int64 `json:"content_length"`
+}
+
+func EchoHandler() http.Handler
+ {
+ // Read body
+ bodyBytes := c.Body() // Fiber's way to get the body
+ bodyString := string(bodyBytes)
+
+ // Try to parse body as JSON
+ var parsedBody interface{}
+ contentTypeHeader := c.Get("Content-Type") // Get Content-Type header
+
+ if len(bodyBytes) > 0 {
+ mediaType, _, err := mime.ParseMediaType(contentTypeHeader)
+ // Only attempt JSON parsing if Content-Type is application/json or it looks like JSON
+ if (err == nil && mediaType == "application/json") || isJSONLike(bodyString) {
+ if jsonErr := json.Unmarshal(bodyBytes, &parsedBody); jsonErr != nil {
+ // If JSON parsing fails, parsedBody remains nil.
+ // It will be set to bodyString later if it's still nil.
+ parsedBody = nil
+ }
+ }
+ }
+
+ if parsedBody == nil && len(bodyBytes) > 0 {
+ // For non-JSON bodies or if JSON parsing failed, use the raw string
+ parsedBody = bodyString
+ }
+
+ // Query Parameters
+ queryParams, _ := url.ParseQuery(string(c.Request().URI().QueryString()))
+
+ // Headers - ensuring canonical keys like net/http.Header
+ headers := make(http.Header)
+ c.Context().Request.Header.VisitAll(func(key, value []byte) {
+ k := string(key)
+ v := string(value)
+ headers.Add(k, v) // http.Header.Add appends, and canonicalizes the key on first Set/Add
+ })
+
+ // Create our response structure
+ echo := RequestEcho{
+ URL: c.OriginalURL(), // Path and query string
+ Method: c.Method(),
+ Path: c.Path(),
+ QueryParams: queryParams,
+ Headers: headers,
+ Body: parsedBody,
+ BodyRaw: bodyString,
+ RemoteAddr: c.Context().RemoteAddr().String(), // IP and Port
+ ContentType: contentTypeHeader,
+ ContentLength: int64(c.Context().Request.Header.ContentLength()),
+ }
+
+ // Marshal to JSON and write response
+ jsonResponse, err := json.MarshalIndent(echo, "", " ")
+ if err != nil {
+ log.Printf("Error creating JSON response: %v", err)
+ return c.Status(http.StatusInternalServerError).SendString("Error creating JSON response")
+ }
+
+ c.Set("Content-Type", "application/json")
+ c.Status(http.StatusOK)
+ // Log the request to stdout
+ // fmt.Printf("Received %s request to %s\n", c.Method(), c.Path())
+ return c.Send(jsonResponse)
+}
diff --git a/crud/.server/crud b/crud/.server/crud
new file mode 100755
index 0000000..86fa185
Binary files /dev/null and b/crud/.server/crud differ
diff --git a/crud/.server/datastar/consts.go b/crud/.server/datastar/consts.go
new file mode 100644
index 0000000..9b896fa
--- /dev/null
+++ b/crud/.server/datastar/consts.go
@@ -0,0 +1,115 @@
+// This is auto-generated by Datastar. DO NOT EDIT.
+
+package datastar
+
+import "time"
+
+const (
+ DatastarKey = "datastar"
+ Version = "1.0.0-beta.11"
+ VersionClientByteSize = 40026
+ VersionClientByteSizeGzip = 14900
+
+ //region Default durations
+
+ // The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE.
+ DefaultSseRetryDuration = 1000 * time.Millisecond
+
+ //endregion Default durations
+
+ //region Default strings
+
+ // The default attributes for element use when executing scripts. It is a set of key-value pairs delimited by a newline \\n character.
+ DefaultExecuteScriptAttributes = "type module"
+
+ //endregion Default strings
+
+ //region Dataline literals
+ SelectorDatalineLiteral = "selector "
+ MergeModeDatalineLiteral = "mergeMode "
+ FragmentsDatalineLiteral = "fragments "
+ UseViewTransitionDatalineLiteral = "useViewTransition "
+ SignalsDatalineLiteral = "signals "
+ OnlyIfMissingDatalineLiteral = "onlyIfMissing "
+ PathsDatalineLiteral = "paths "
+ ScriptDatalineLiteral = "script "
+ AttributesDatalineLiteral = "attributes "
+ AutoRemoveDatalineLiteral = "autoRemove "
+ //endregion Dataline literals
+)
+
+var (
+ //region Default booleans
+
+ // Should fragments be merged using the ViewTransition API?
+ DefaultFragmentsUseViewTransitions = false
+
+ // Should a given set of signals merge if they are missing?
+ DefaultMergeSignalsOnlyIfMissing = false
+
+ // Should script element remove itself after execution?
+ DefaultExecuteScriptAutoRemove = true
+
+ //endregion Default booleans
+)
+
+//region Enums
+
+//region The mode in which a fragment is merged into the DOM.
+type FragmentMergeMode string
+
+const (
+ // Default value for FragmentMergeMode
+ // Morphs the fragment into the existing element using idiomorph.
+ DefaultFragmentMergeMode = FragmentMergeModeMorph
+
+ // Morphs the fragment into the existing element using idiomorph.
+ FragmentMergeModeMorph FragmentMergeMode = "morph"
+
+ // Replaces the inner HTML of the existing element.
+ FragmentMergeModeInner FragmentMergeMode = "inner"
+
+ // Replaces the outer HTML of the existing element.
+ FragmentMergeModeOuter FragmentMergeMode = "outer"
+
+ // Prepends the fragment to the existing element.
+ FragmentMergeModePrepend FragmentMergeMode = "prepend"
+
+ // Appends the fragment to the existing element.
+ FragmentMergeModeAppend FragmentMergeMode = "append"
+
+ // Inserts the fragment before the existing element.
+ FragmentMergeModeBefore FragmentMergeMode = "before"
+
+ // Inserts the fragment after the existing element.
+ FragmentMergeModeAfter FragmentMergeMode = "after"
+
+ // Upserts the attributes of the existing element.
+ FragmentMergeModeUpsertAttributes FragmentMergeMode = "upsertAttributes"
+
+)
+//endregion FragmentMergeMode
+
+//region The type protocol on top of SSE which allows for core pushed based communication between the server and the client.
+type EventType string
+
+const (
+ // An event for merging HTML fragments into the DOM.
+ EventTypeMergeFragments EventType = "datastar-merge-fragments"
+
+ // An event for merging signals.
+ EventTypeMergeSignals EventType = "datastar-merge-signals"
+
+ // An event for removing HTML fragments from the DOM.
+ EventTypeRemoveFragments EventType = "datastar-remove-fragments"
+
+ // An event for removing signals.
+ EventTypeRemoveSignals EventType = "datastar-remove-signals"
+
+ // An event for executing elements in the browser.
+ EventTypeExecuteScript EventType = "datastar-execute-script"
+
+)
+//endregion EventType
+
+//endregion Enums
\ No newline at end of file
diff --git a/crud/.server/datastar/execute-script-sugar.go b/crud/.server/datastar/execute-script-sugar.go
new file mode 100644
index 0000000..310b9fa
--- /dev/null
+++ b/crud/.server/datastar/execute-script-sugar.go
@@ -0,0 +1,234 @@
+package datastar
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+)
+
+// ConsoleLog is a convenience method for [see.ExecuteScript].
+// It is equivalent to calling [see.ExecuteScript] with [see.WithScript] option set to `console.log(msg)`.
+func (sse *ServerSentEventGenerator) ConsoleLog(msg string, opts ...ExecuteScriptOption) error {
+ call := fmt.Sprintf("console.log(%q)", msg)
+ return sse.ExecuteScript(call, opts...)
+}
+
+// ConsoleLogf is a convenience method for [see.ExecuteScript].
+// It is equivalent to calling [see.ExecuteScript] with [see.WithScript] option set to `console.log(fmt.Sprintf(format, args...))`.
+func (sse *ServerSentEventGenerator) ConsoleLogf(format string, args ...any) error {
+ return sse.ConsoleLog(fmt.Sprintf(format, args...))
+}
+
+// ConsoleError is a convenience method for [see.ExecuteScript].
+// It is equivalent to calling [see.ExecuteScript] with [see.WithScript] option set to `console.error(msg)`.
+func (sse *ServerSentEventGenerator) ConsoleError(err error, opts ...ExecuteScriptOption) error {
+ call := fmt.Sprintf("console.error(%q)", err.Error())
+ return sse.ExecuteScript(call, opts...)
+}
+
+// Redirectf is a convenience method for [see.ExecuteScript].
+// It sends a redirect event to the client formatted using [fmt.Sprintf].
+func (sse *ServerSentEventGenerator) Redirectf(format string, args ...any) error {
+ url := fmt.Sprintf(format, args...)
+ return sse.Redirect(url)
+}
+
+// Redirect is a convenience method for [see.ExecuteScript].
+// It sends a redirect event to the client .
+func (sse *ServerSentEventGenerator) Redirect(url string, opts ...ExecuteScriptOption) error {
+ js := fmt.Sprintf("setTimeout(() => window.location.href = %q)", url)
+ return sse.ExecuteScript(js, opts...)
+}
+
+// dispatchCustomEventOptions holds the configuration data
+// modified by [DispatchCustomEventOption]s
+// for dispatching custom events to the client.
+type dispatchCustomEventOptions struct {
+ EventID string
+ RetryDuration time.Duration
+ Selector string
+ Bubbles bool
+ Cancelable bool
+ Composed bool
+}
+
+// DispatchCustomEventOption configures one custom
+// server-sent event.
+type DispatchCustomEventOption func(*dispatchCustomEventOptions)
+
+// WithDispatchCustomEventEventID configures an optional event ID for the custom event.
+// The client message field [lastEventId] will be set to this value.
+// If the next event does not have an event ID, the last used event ID will remain.
+//
+// [lastEventId]: https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/lastEventId
+func WithDispatchCustomEventEventID(id string) DispatchCustomEventOption {
+ return func(o *dispatchCustomEventOptions) {
+ o.EventID = id
+ }
+}
+
+// WithDispatchCustomEventRetryDuration overrides the [DefaultSseRetryDuration] for one custom event.
+func WithDispatchCustomEventRetryDuration(retryDuration time.Duration) DispatchCustomEventOption {
+ return func(o *dispatchCustomEventOptions) {
+ o.RetryDuration = retryDuration
+ }
+}
+
+// WithDispatchCustomEventSelector replaces the default custom event target `document` with a
+// [CSS selector]. If the selector matches multiple HTML elements, the event will be dispatched
+// from each one. For example, if the selector is `#my-element`, the event will be dispatched
+// from the element with the ID `my-element`. If the selector is `main > section`, the event will be dispatched
+// from each `` element which is a direct child of the `` element.
+//
+// [CSS selector]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_selectors
+func WithDispatchCustomEventSelector(selector string) DispatchCustomEventOption {
+ return func(o *dispatchCustomEventOptions) {
+ o.Selector = selector
+ }
+}
+
+// WithDispatchCustomEventBubbles overrides the default custom [event bubbling] `true` value.
+// Setting bubbling to `false` is equivalent to calling `event.stopPropagation()` Javascript
+// command on the client side for the dispatched event. This prevents the event from triggering
+// event handlers of its parent elements.
+//
+// [event bubbling]: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Scripting/Event_bubbling
+func WithDispatchCustomEventBubbles(bubbles bool) DispatchCustomEventOption {
+ return func(o *dispatchCustomEventOptions) {
+ o.Bubbles = bubbles
+ }
+}
+
+// WithDispatchCustomEventCancelable overrides the default custom [event cancelability] `true` value.
+// Setting cancelability to `false` is blocks `event.preventDefault()` Javascript
+// command on the client side for the dispatched event.
+//
+// [event cancelability]: https://developer.mozilla.org/en-US/docs/Web/API/Event/cancelable
+func WithDispatchCustomEventCancelable(cancelable bool) DispatchCustomEventOption {
+ return func(o *dispatchCustomEventOptions) {
+ o.Cancelable = cancelable
+ }
+}
+
+// WithDispatchCustomEventComposed overrides the default custom [event composed] `true` value.
+// It indicates whether or not the event will propagate across the shadow HTML DOM boundary into
+// the document DOM tree. When `false`, the shadow root will be the last node to be offered the event.
+//
+// [event composed]: https://developer.mozilla.org/en-US/docs/Web/API/Event/composed
+func WithDispatchCustomEventComposed(composed bool) DispatchCustomEventOption {
+ return func(o *dispatchCustomEventOptions) {
+ o.Composed = composed
+ }
+}
+
+// DispatchCustomEvent is a convenience method for dispatching a custom event by executing
+// a client side script via [sse.ExecuteScript] call. The detail struct is marshaled to JSON and
+// passed as a parameter to the event.
+func (sse *ServerSentEventGenerator) DispatchCustomEvent(eventName string, detail any, opts ...DispatchCustomEventOption) error {
+ if eventName == "" {
+ return fmt.Errorf("eventName is required")
+ }
+
+ detailsJSON, err := json.Marshal(detail)
+ if err != nil {
+ return fmt.Errorf("failed to marshal detail: %w", err)
+ }
+
+ const defaultSelector = "document"
+ options := dispatchCustomEventOptions{
+ EventID: "",
+ RetryDuration: DefaultSseRetryDuration,
+ Selector: defaultSelector,
+ Bubbles: true,
+ Cancelable: true,
+ Composed: true,
+ }
+
+ for _, opt := range opts {
+ opt(&options)
+ }
+
+ elementsJS := `[document]`
+ if options.Selector != "" && options.Selector != defaultSelector {
+ elementsJS = fmt.Sprintf(`document.querySelectorAll(%q)`, options.Selector)
+ }
+
+ js := fmt.Sprintf(`
+const elements = %s
+
+const event = new CustomEvent(%q, {
+ bubbles: %t,
+ cancelable: %t,
+ composed: %t,
+ detail: %s,
+});
+
+elements.forEach((element) => {
+ element.dispatchEvent(event);
+});
+ `,
+ elementsJS,
+ eventName,
+ options.Bubbles,
+ options.Cancelable,
+ options.Composed,
+ string(detailsJSON),
+ )
+
+ executeOptions := make([]ExecuteScriptOption, 0)
+ if options.EventID != "" {
+ executeOptions = append(executeOptions, WithExecuteScriptEventID(options.EventID))
+ }
+ if options.RetryDuration != 0 {
+ executeOptions = append(executeOptions, WithExecuteScriptRetryDuration(options.RetryDuration))
+ }
+
+ return sse.ExecuteScript(js, executeOptions...)
+
+}
+
+// ReplaceURL replaces the current URL in the browser's history.
+func (sse *ServerSentEventGenerator) ReplaceURL(u url.URL, opts ...ExecuteScriptOption) error {
+ js := fmt.Sprintf(`window.history.replaceState({}, "", %q)`, u.String())
+ return sse.ExecuteScript(js, opts...)
+}
+
+// ReplaceURLQuerystring is a convenience wrapper for [sse.ReplaceURL] that replaces the query
+// string of the current URL request with new a new query built from the provided values.
+func (sse *ServerSentEventGenerator) ReplaceURLQuerystring(r *http.Request, values url.Values, opts ...ExecuteScriptOption) error {
+ // TODO: rename this function to ReplaceURLQuery
+ u := *r.URL
+ u.RawQuery = values.Encode()
+ return sse.ReplaceURL(u, opts...)
+}
+
+// Prefetch is a convenience wrapper for [sse.ExecuteScript] that prefetches the provided links.
+// It follows the Javascript [speculation rules API] prefetch specification.
+//
+// [speculation rules API]: https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API
+func (sse *ServerSentEventGenerator) Prefetch(urls ...string) error {
+ wrappedURLs := make([]string, len(urls))
+ for i, url := range urls {
+ wrappedURLs[i] = fmt.Sprintf(`"%s"`, url)
+ }
+ script := fmt.Sprintf(`
+{
+ "prefetch": [
+ {
+ "source": "list",
+ "urls": [
+ %s
+ ]
+ }
+ ]
+}
+ `, strings.Join(wrappedURLs, ",\n\t\t\t\t"))
+ return sse.ExecuteScript(
+ script,
+ WithExecuteScriptAutoRemove(false),
+ WithExecuteScriptAttributes("type speculationrules"),
+ )
+}
diff --git a/crud/.server/datastar/execute.go b/crud/.server/datastar/execute.go
new file mode 100644
index 0000000..5b838c6
--- /dev/null
+++ b/crud/.server/datastar/execute.go
@@ -0,0 +1,112 @@
+package datastar
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+)
+
+// executeScriptOptions hold script options that will be translated to [SSEEventOptions].
+type executeScriptOptions struct {
+ EventID string
+ RetryDuration time.Duration
+ Attributes []string
+ AutoRemove *bool
+}
+
+// ExecuteScriptOption configures script execution event that will be sent to the client.
+type ExecuteScriptOption func(*executeScriptOptions)
+
+// WithExecuteScriptEventID configures an optional event ID for the script execution event.
+// The client message field [lastEventId] will be set to this value.
+// If the next event does not have an event ID, the last used event ID will remain.
+//
+// [lastEventId]: https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent/lastEventId
+func WithExecuteScriptEventID(id string) ExecuteScriptOption {
+ return func(o *executeScriptOptions) {
+ o.EventID = id
+ }
+}
+
+// WithExecuteScriptRetryDuration overrides the [DefaultSseRetryDuration] for this script
+// execution only.
+func WithExecuteScriptRetryDuration(retryDuration time.Duration) ExecuteScriptOption {
+ return func(o *executeScriptOptions) {
+ o.RetryDuration = retryDuration
+ }
+}
+
+// WithExecuteScriptAttributes overrides the default script attribute
+// value `type module`, which renders as `
+
+
+
+
+ The CRUD Example
+
+
+
+
+
+
+
+