Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Ludwig Lehnert committed Sep 21, 2024
0 parents commit 6a7ac24
Show file tree
Hide file tree
Showing 6 changed files with 348 additions and 0 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# LDB (lehnert-db) Framework

SQLite3 DBMS wrapper as-a-library inspired by [pocketbase](https://pocketbase.io/).

## Key Features

- [ ] Schema-First approach with migrations directly in golang
- [ ] Consistency for relations **at DB level**
- [ ] Fine grained user based access control
- [ ] Builtin REST & GraphQL APIs
- [ ] File storage using Google & AWS storage buckets (or your hard drive)
- [ ] Portability as an API frontend for PostgreSQL (maybe also MySQL)

## Planned client SDKs

- [ ] Dart/Flutter
- [ ] JS/TS
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module lehnert.dev/ldb

go 1.22.7
38 changes: 38 additions & 0 deletions id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package ldb

import (
"crypto/rand"
"fmt"
"strings"
"time"
)

func GenerateId() string {
// MYSQL: CONCAT(UNHEX(CONV(ROUND(UNIX_TIMESTAMP(CURTIME(4))*1000), 10, 16)), RANDOM_BYTES(10))

timestamp := int64(time.Now().UnixMilli() * 1000)

entropy := make([]byte, 8)
rand.Read(entropy)

return fmt.Sprintf("%x%x", timestamp, entropy)
}

func ValidateId(value any) error {
str, ok := value.(string)
if !ok {
return fmt.Errorf("invalid id, expected string value")
}

const requiredLen = 31
if len(str) != requiredLen {
return fmt.Errorf("invalid id, expected string of length %v", requiredLen)
}

str = strings.ToLower(str)
if len(strings.Trim(str, "0123456789abcdef")) != 0 {
return fmt.Errorf("invalid id, expcted hex string")
}

return nil
}
34 changes: 34 additions & 0 deletions ldb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package ldb

import "fmt"

type App struct {
Migrations map[string]*Migration
DatabaseService *DatabaseService
HttpService *HttpService
}

type Migration struct {
Up func() error
Down func() error
}

type DatabaseService interface {
CreateCollection(name string, schema CollectionSchema) error
DropCollection(name string) error
}

type HttpService interface {
}

func (app *App) RegisterMigration(name string, migration Migration) {
if app.Migrations == nil {
app.Migrations = map[string]*Migration{}
}

app.Migrations[name] = &migration
}

func (app *App) Start() {

}
Empty file added migration.go
Empty file.
256 changes: 256 additions & 0 deletions schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package ldb

import (
"fmt"
"regexp"
"slices"
"strings"
"time"
)

type CollectionSchema struct {
Name string
Fields []*SchemaField
}

type SchemaField struct {
Name string
Type SchemaFieldType
}

type SchemaFieldType interface {
GetName() string
ValidateValue(value any) (any, error)
}

func validateNullable(nullable bool, value any) error {
if value == nil && !nullable {
return fmt.Errorf("invalid value, expected non-null")
}

return nil
}

type TextFieldType struct {
Nullable bool
DefaultValue *string
MaxLength *int
MinLength *int
Pattern *string
}

func (fieldType TextFieldType) ValidateValue(value any) (any, error) {
if err := validateNullable(fieldType.Nullable, value); err != nil {
return nil, err
}

if value == nil {
return fieldType.DefaultValue, nil
}

str, ok := value.(string)
if !ok {
return nil, fmt.Errorf("invalid value, expected string")
}

if fieldType.MaxLength != nil && len(str) > *fieldType.MaxLength {
return nil, fmt.Errorf("value too long, max length is %v", *fieldType.MaxLength)
}

if fieldType.MinLength != nil && len(str) < *fieldType.MaxLength {
return nil, fmt.Errorf("value too short, min length is %v", *fieldType.MinLength)
}

if fieldType.Pattern != nil {
if _, err := regexp.MatchString(*fieldType.Pattern, str); err != nil {
return nil, fmt.Errorf("value does not match pattern, pattern is %v", *fieldType.Pattern)
}
}

return &str, nil
}

type IntFieldType struct {
Nullable bool
DefaultValue *int64
MinValue *int64
MaxValue *int64
}

func (fieldType IntFieldType) ValidateValue(value any) (any, error) {
if err := validateNullable(fieldType.Nullable, value); err != nil {
return nil, err
}

if value == nil {
return fieldType.DefaultValue, nil
}

i, ok := value.(int64)
if !ok {
return nil, fmt.Errorf("invalid value, expected integer")
}

if fieldType.MinValue != nil && i < *fieldType.MinValue {
return nil, fmt.Errorf("value too small, min value is %v", *fieldType.MinValue)
}

if fieldType.MaxValue != nil && i > *fieldType.MaxValue {
return nil, fmt.Errorf("value too big, max value is %v", *fieldType.MaxValue)
}

return &i, nil
}

type FloatFieldType struct {
Nullable bool
DefaultValue *float64
MinValue *float64
MaxValue *float64
}

func (fieldType FloatFieldType) ValidateValue(value any) (any, error) {
if err := validateNullable(fieldType.Nullable, value); err != nil {
return nil, err
}

if value == nil {
return fieldType.DefaultValue, nil
}

f, ok := value.(float64)
if !ok {
return nil, fmt.Errorf("invalid value, expected float")
}

if fieldType.MinValue != nil && f < *fieldType.MinValue {
return nil, fmt.Errorf("value too small, min value is %v", *fieldType.MinValue)
}

if fieldType.MaxValue != nil && f > *fieldType.MaxValue {
return nil, fmt.Errorf("value too big, max value is %v", *fieldType.MaxValue)
}

return &f, nil
}

type BoolFieldType struct {
Nullable bool
DefaultValue *bool
}

func (fieldType BoolFieldType) ValidateValue(value any) (any, error) {
if err := validateNullable(fieldType.Nullable, value); err != nil {
return nil, err
}

if value == nil {
return fieldType.DefaultValue, nil
}

b, ok := value.(bool)
if !ok {
return nil, fmt.Errorf("invalid value, expected bool")
}

return &b, nil
}

type DateTimeFieldType struct {
Nullable bool
CreateDefaultValue func() time.Time
CreateMinValue func() time.Time
CreateMaxValue func() time.Time
}

func (fieldType DateTimeFieldType) ValidateValue(value any) (any, error) {
if err := validateNullable(fieldType.Nullable, value); err != nil {
return nil, err
}

if value == nil {
if fieldType.CreateDefaultValue != nil {
defaultValue := fieldType.CreateDefaultValue()
return &defaultValue, nil
}

return nil, nil
}

const timeFormat = time.RFC3339
const timeFormatName = "RFC-3339"

d, ok := value.(time.Time)
if !ok {
str, _ := value.(string)

var err error
if d, err = time.Parse(timeFormat, str); err != nil {
return nil, fmt.Errorf("invalid value, expected datetime or %s datetime string", timeFormatName)
}
}

if fieldType.CreateMinValue != nil {
minValue := fieldType.CreateMinValue()
if d.Before(minValue) {
return nil, fmt.Errorf("value too early, min value is %s", d.Format(timeFormat))
}
}

if fieldType.CreateMaxValue != nil {
maxValue := fieldType.CreateMaxValue()
if d.After(maxValue) {
return nil, fmt.Errorf("value too late, max value is %s", d.Format(timeFormat))
}
}

return &d, nil
}

type EnumFieldType struct {
Nullable bool
DefaultValue *string
EnumValues []string
}

func (fieldType EnumFieldType) ValidateValue(value any) (any, error) {
if fieldType.DefaultValue != nil && !slices.Contains(fieldType.EnumValues, *fieldType.DefaultValue) {
return nil, fmt.Errorf("configuration error, invalid default value")
}

if err := validateNullable(fieldType.Nullable, value); err != nil {
return nil, err
}

if value == nil {
return fieldType.DefaultValue, nil
}

str, ok := value.(string)
if !ok || !slices.Contains(fieldType.EnumValues, str) {
return nil, fmt.Errorf("invalid value, expected one of [%s]", strings.Join(fieldType.EnumValues, ", "))
}

return str, nil
}

type SingleRelationFieldType struct {
Nullable bool
Collection string
}

func (fieldType SingleRelationFieldType) ValidateValue(value any) (any, error) {
if err := validateNullable(fieldType.Nullable, value); err != nil {
return nil, err
}

if value == nil {
return nil, nil
}

if err := ValidateId(value); err != nil {
return nil, err
}

return value, nil
}

0 comments on commit 6a7ac24

Please sign in to comment.