feat: continue basic setup
Some checks failed
Go build / build (push) Failing after 1m58s

- setup ent scheme
- add command to create users
- document API
- add helpers to create migrations
- add command to run migrations
- add basic compose file
This commit is contained in:
Peter 2024-06-19 21:19:37 +02:00
parent 5a5d3a12b5
commit 9ea9a8f658
Signed by: prskr
GPG key ID: F56BED6903BC5E37
81 changed files with 3051 additions and 74 deletions

4
.devcontainer/.env Normal file
View file

@ -0,0 +1,4 @@
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=postgres
POSTGRES_HOSTNAME=localhost

13
.devcontainer/Dockerfile Normal file
View file

@ -0,0 +1,13 @@
FROM mcr.microsoft.com/devcontainers/go:1-1.22-bookworm
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>
# [Optional] Uncomment the next lines to use go get to install anything else you need
# USER vscode
# RUN go get -x <your-dependency-or-tool>
# USER root
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1

36
.devcontainer/compose.yml Normal file
View file

@ -0,0 +1,36 @@
volumes:
postgres-data:
services:
app:
build:
context: .
dockerfile: Dockerfile
env_file:
# Ensure that the variables in .env match the same variables in devcontainer.json
- .env
volumes:
- ../..:/workspaces:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
db:
image: postgres:latest
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
env_file:
# Ensure that the variables in .env match the same variables in devcontainer.json
- .env
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)

View file

@ -0,0 +1,23 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/go-postgres
{
"name": "Go & PostgreSQL",
"dockerComposeFile": "compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}"
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Configure tool-specific properties.
// "customizations": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [5432],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "go version",
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

27
.editorconfig Normal file
View file

@ -0,0 +1,27 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
tab_width = 4
indent_style = space
insert_final_newline = false
max_line_length = 120
trim_trailing_whitespace = true
[*.go]
indent_style = tab
ij_smart_tabs = true
ij_go_GROUP_CURRENT_PROJECT_IMPORTS = true
ij_go_group_stdlib_imports = true
ij_go_import_sorting = goimports
ij_go_local_group_mode = project
ij_go_move_all_imports_in_one_declaration = true
ij_go_move_all_stdlib_imports_in_one_group = true
ij_go_remove_redundant_import_aliases = true
[*.{yml,yaml}]
indent_size = 2
tab_width = 2
insert_final_newline = true

5
.gitignore vendored
View file

@ -25,4 +25,7 @@ data/
# IDE configs
.idea/
.vscode/
# Generated files
generated/
internal/ent/

11
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,11 @@
{
"recommendations": [
"golang.Go",
"a-h.templ",
"oderwat.indent-rainbow",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"redhat.vscode-yaml",
"ms-azuretools.vscode-docker"
]
}

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"redhat.telemetry.enabled": false
}

432
assets/api/ent.graphql Normal file
View file

@ -0,0 +1,432 @@
directive @goField(forceResolver: Boolean, name: String, omittable: Boolean) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
directive @goModel(model: String, models: [String!], forceGenerate: Boolean) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION
"""
Define a Relay Cursor type:
https://relay.dev/graphql/connections.htm#sec-Cursor
"""
scalar Cursor
type Index implements Node {
id: ID!
createTime: Time!
updateTime: Time!
siteID: ID!
name: String!
revision: String!
index: Site!
}
"""
IndexWhereInput is used for filtering Index objects.
Input was generated by ent.
"""
input IndexWhereInput {
not: IndexWhereInput
and: [IndexWhereInput!]
or: [IndexWhereInput!]
"""
id field predicates
"""
id: ID
idNEQ: ID
idIn: [ID!]
idNotIn: [ID!]
idGT: ID
idGTE: ID
idLT: ID
idLTE: ID
"""
create_time field predicates
"""
createTime: Time
createTimeNEQ: Time
createTimeIn: [Time!]
createTimeNotIn: [Time!]
createTimeGT: Time
createTimeGTE: Time
createTimeLT: Time
createTimeLTE: Time
"""
update_time field predicates
"""
updateTime: Time
updateTimeNEQ: Time
updateTimeIn: [Time!]
updateTimeNotIn: [Time!]
updateTimeGT: Time
updateTimeGTE: Time
updateTimeLT: Time
updateTimeLTE: Time
"""
site_id field predicates
"""
siteID: ID
siteIDNEQ: ID
siteIDIn: [ID!]
siteIDNotIn: [ID!]
"""
name field predicates
"""
name: String
nameNEQ: String
nameIn: [String!]
nameNotIn: [String!]
nameGT: String
nameGTE: String
nameLT: String
nameLTE: String
nameContains: String
nameHasPrefix: String
nameHasSuffix: String
nameEqualFold: String
nameContainsFold: String
"""
revision field predicates
"""
revision: String
revisionNEQ: String
revisionIn: [String!]
revisionNotIn: [String!]
revisionGT: String
revisionGTE: String
revisionLT: String
revisionLTE: String
revisionContains: String
revisionHasPrefix: String
revisionHasSuffix: String
revisionEqualFold: String
revisionContainsFold: String
"""
index edge predicates
"""
hasIndex: Boolean
hasIndexWith: [SiteWhereInput!]
}
"""
An object with an ID.
Follows the [Relay Global Object Identification Specification](https://relay.dev/graphql/objectidentification.htm)
"""
interface Node @goModel(model: "code.icb4dc0.de/prskr/searcherside/internal/ent.Noder") {
"""
The id of the object.
"""
id: ID!
}
"""
Possible directions in which to order a list of items when provided an `orderBy` argument.
"""
enum OrderDirection {
"""
Specifies an ascending order for a given `orderBy` argument.
"""
ASC
"""
Specifies a descending order for a given `orderBy` argument.
"""
DESC
}
"""
Information about pagination in a connection.
https://relay.dev/graphql/connections.htm#sec-undefined.PageInfo
"""
type PageInfo {
"""
When paginating forwards, are there more items?
"""
hasNextPage: Boolean!
"""
When paginating backwards, are there more items?
"""
hasPreviousPage: Boolean!
"""
When paginating backwards, the cursor to continue.
"""
startCursor: Cursor
"""
When paginating forwards, the cursor to continue.
"""
endCursor: Cursor
}
type Query {
"""
Fetches an object given its ID.
"""
node(
"""
ID of the object.
"""
id: ID!
): Node
"""
Lookup nodes by a list of IDs.
"""
nodes(
"""
The list of node IDs.
"""
ids: [ID!]!
): [Node]!
sites: [Site!]!
users: [User!]!
}
type Site implements Node {
id: ID!
createTime: Time!
updateTime: Time!
name: String!
siteMembers: [UserStaff!]
indices: [Index!]
}
"""
SiteWhereInput is used for filtering Site objects.
Input was generated by ent.
"""
input SiteWhereInput {
not: SiteWhereInput
and: [SiteWhereInput!]
or: [SiteWhereInput!]
"""
id field predicates
"""
id: ID
idNEQ: ID
idIn: [ID!]
idNotIn: [ID!]
idGT: ID
idGTE: ID
idLT: ID
idLTE: ID
"""
create_time field predicates
"""
createTime: Time
createTimeNEQ: Time
createTimeIn: [Time!]
createTimeNotIn: [Time!]
createTimeGT: Time
createTimeGTE: Time
createTimeLT: Time
createTimeLTE: Time
"""
update_time field predicates
"""
updateTime: Time
updateTimeNEQ: Time
updateTimeIn: [Time!]
updateTimeNotIn: [Time!]
updateTimeGT: Time
updateTimeGTE: Time
updateTimeLT: Time
updateTimeLTE: Time
"""
name field predicates
"""
name: String
nameNEQ: String
nameIn: [String!]
nameNotIn: [String!]
nameGT: String
nameGTE: String
nameLT: String
nameLTE: String
nameContains: String
nameHasPrefix: String
nameHasSuffix: String
nameEqualFold: String
nameContainsFold: String
"""
site_members edge predicates
"""
hasSiteMembers: Boolean
hasSiteMembersWith: [UserStaffWhereInput!]
"""
indices edge predicates
"""
hasIndices: Boolean
hasIndicesWith: [IndexWhereInput!]
}
"""
The builtin Time type
"""
scalar Time
type User implements Node {
id: ID!
createTime: Time!
updateTime: Time!
email: String!
givenName: String
surname: String
staffs: [UserStaff!]
}
type UserStaff implements Node {
id: ID!
createTime: Time!
updateTime: Time!
userID: ID!
siteID: ID!
users: User!
site: Site!
}
"""
UserStaffWhereInput is used for filtering UserStaff objects.
Input was generated by ent.
"""
input UserStaffWhereInput {
not: UserStaffWhereInput
and: [UserStaffWhereInput!]
or: [UserStaffWhereInput!]
"""
id field predicates
"""
id: ID
idNEQ: ID
idIn: [ID!]
idNotIn: [ID!]
idGT: ID
idGTE: ID
idLT: ID
idLTE: ID
"""
create_time field predicates
"""
createTime: Time
createTimeNEQ: Time
createTimeIn: [Time!]
createTimeNotIn: [Time!]
createTimeGT: Time
createTimeGTE: Time
createTimeLT: Time
createTimeLTE: Time
"""
update_time field predicates
"""
updateTime: Time
updateTimeNEQ: Time
updateTimeIn: [Time!]
updateTimeNotIn: [Time!]
updateTimeGT: Time
updateTimeGTE: Time
updateTimeLT: Time
updateTimeLTE: Time
"""
user_id field predicates
"""
userID: ID
userIDNEQ: ID
userIDIn: [ID!]
userIDNotIn: [ID!]
"""
site_id field predicates
"""
siteID: ID
siteIDNEQ: ID
siteIDIn: [ID!]
siteIDNotIn: [ID!]
"""
users edge predicates
"""
hasUsers: Boolean
hasUsersWith: [UserWhereInput!]
"""
site edge predicates
"""
hasSite: Boolean
hasSiteWith: [SiteWhereInput!]
}
"""
UserWhereInput is used for filtering User objects.
Input was generated by ent.
"""
input UserWhereInput {
not: UserWhereInput
and: [UserWhereInput!]
or: [UserWhereInput!]
"""
id field predicates
"""
id: ID
idNEQ: ID
idIn: [ID!]
idNotIn: [ID!]
idGT: ID
idGTE: ID
idLT: ID
idLTE: ID
"""
create_time field predicates
"""
createTime: Time
createTimeNEQ: Time
createTimeIn: [Time!]
createTimeNotIn: [Time!]
createTimeGT: Time
createTimeGTE: Time
createTimeLT: Time
createTimeLTE: Time
"""
update_time field predicates
"""
updateTime: Time
updateTimeNEQ: Time
updateTimeIn: [Time!]
updateTimeNotIn: [Time!]
updateTimeGT: Time
updateTimeGTE: Time
updateTimeLT: Time
updateTimeLTE: Time
"""
email field predicates
"""
email: String
emailNEQ: String
emailIn: [String!]
emailNotIn: [String!]
emailGT: String
emailGTE: String
emailLT: String
emailLTE: String
emailContains: String
emailHasPrefix: String
emailHasSuffix: String
emailEqualFold: String
emailContainsFold: String
"""
given_name field predicates
"""
givenName: String
givenNameNEQ: String
givenNameIn: [String!]
givenNameNotIn: [String!]
givenNameGT: String
givenNameGTE: String
givenNameLT: String
givenNameLTE: String
givenNameContains: String
givenNameHasPrefix: String
givenNameHasSuffix: String
givenNameIsNil: Boolean
givenNameNotNil: Boolean
givenNameEqualFold: String
givenNameContainsFold: String
"""
surname field predicates
"""
surname: String
surnameNEQ: String
surnameIn: [String!]
surnameNotIn: [String!]
surnameGT: String
surnameGTE: String
surnameLT: String
surnameLTE: String
surnameContains: String
surnameHasPrefix: String
surnameHasSuffix: String
surnameIsNil: Boolean
surnameNotNil: Boolean
surnameEqualFold: String
surnameContainsFold: String
"""
staffs edge predicates
"""
hasStaffs: Boolean
hasStaffsWith: [UserStaffWhereInput!]
}

View file

@ -0,0 +1,164 @@
openapi: 3.1.0
info:
title: Searcherside
license:
name: MIT
url: https://code.icb4dc0.de/prskr/searcherside/src/branch/main/LICENSE
version: 0.1.0
servers:
- url: http://localhost:3000/api/v1
tags:
- name: index
description: Interact with indices
- name: search
description: Search in indices
paths:
/site/{siteId}/index/{indexName}:
put:
tags:
- index
summary: Create a new index by uploading an archive of all documents to index
operationId: upsertIndexForSite
parameters:
- name: siteId
in: path
description: ID of the site for which to upsert a new index
required: true
schema:
type: string
format: uuid
- name: indexName
in: path
description: Name of the index to create
required: true
examples:
Writerside:
description: |
Writerside index of the format <module>/<instance>
value: web/g
schema:
type: string
requestBody:
content:
multipart/form-data:
schema:
type: object
properties:
filename:
type: string
format: binary
responses:
'202':
description: Accepted upload of file
/site/{siteId}/preview-search/{indexName}:
get:
tags:
- search
summary: Search in the latest index version of the specified site
security:
- jwtAuth: []
operationId: previewSearch
parameters:
- name: siteId
in: path
description: ID of the site for which to upsert a new index
required: true
schema:
type: string
format: uuid
- name: indexName
in: path
description: Name of the index to create
required: true
examples:
Writerside:
description: |
Writerside index of the format <module>/<instance>
value: web/g
schema:
type: string
- name: query
in: query
description: Search query
required: true
schema:
type: string
- name: isExactSearch
in: query
description: Whether only exact matches should be considered
required: false
schema:
type: boolean
- name: maxHits
in: query
description: Maximum number of items to return
required: false
schema:
type: integer
responses:
'200':
description: Successfully searched in index
content:
application/json:
schema:
$ref: "#/components/schemas/SearchResponse"
components:
schemas:
MatchLevel:
type: string
enum: [none, full]
HighlightResult:
type: object
additionalProperties:
type: object
properties:
value:
type: string
matchLevel:
$ref: "#/components/schemas/MatchLevel"
fullyHighlighted:
type: bool
matchedWords:
type: array
items:
type: string
SearchHit:
type: object
properties:
objectID:
type: string
mainTitle:
type: string
pageTitle:
type: string
url:
type: string
breadcrumbs:
type: string
_snippetResult:
type: object
properties:
content:
type: object
properties:
value:
type: string
matchLevel:
$ref: "#/components/schemas/MatchLevel"
_highlightResult:
$ref: "#/components/schemas/HighlightResult"
SearchResponse:
type: object
properties:
hits:
type: array
items:
$ref: "#/components/schemas/SearchHit"
securitySchemes:
jwtAuth:
type: apiKey
name: api_key
in: header

1
assets/api/types.graphql Normal file
View file

@ -0,0 +1 @@
scalar UUID

9
compose.yml Normal file
View file

@ -0,0 +1,9 @@
services:
postgres:
image: docker.io/postgres:16-alpine
environment:
POSTGRES_USER: searcherside
POSTGRES_PASSWORD: 1n1t-r00t!
POSTGRES_DB: searcherside
ports:
- "5432:5432"

View file

@ -1,3 +1,6 @@
server:
http:
http:
readHeaderTimeout: 15s
db:
driver: postgres
url: 'postgresql://searcherside:1n1t-r00t!@localhost:5432/searcherside?sslmode=disable'

10
core/cq/user.go Normal file
View file

@ -0,0 +1,10 @@
package cq
type CreateUserRequest struct {
Email string
Password []byte
Admin bool
}
type CreateUserResponse struct {
}

12
core/domain/user.go Normal file
View file

@ -0,0 +1,12 @@
package domain
import "github.com/google/uuid"
type User struct {
ID uuid.UUID
Email string
GivenName string
Surname string
IsAdmin bool
}

34
core/ports/db.go Normal file
View file

@ -0,0 +1,34 @@
package ports
import (
"context"
"ariga.io/atlas/sql/migrate"
"code.icb4dc0.de/prskr/searcherside/internal/ent"
"entgo.io/ent/dialect"
)
type Driver string
const (
DriverPostgres = Driver(dialect.Postgres)
DriverSQLite = Driver(dialect.SQLite)
)
func (t Driver) String() string {
return string(t)
}
type MigrationRequest struct {
Driver Driver
URL string
}
type RevisionReadWriter interface {
migrate.RevisionReadWriter
Client() *ent.Client
}
type Migrator interface {
Migrate(ctx context.Context, req MigrationRequest) error
}

6
core/ports/pw_hash.go Normal file
View file

@ -0,0 +1,6 @@
package ports
type PasswordHashAlgorithm interface {
Hash(password []byte) ([]byte, error)
Validate(password []byte, hash []byte) error
}

View file

@ -0,0 +1,26 @@
package ports
import (
"context"
"io"
"github.com/google/uuid"
"code.icb4dc0.de/prskr/searcherside/core/cq"
"code.icb4dc0.de/prskr/searcherside/core/domain"
)
type UserReadRepository interface {
UserByID(ctx context.Context, id uuid.UUID) (*domain.User, error)
UserByEmail(ctx context.Context, email string) (*domain.User, error)
}
type UserWriteRepository interface {
CreateUser(ctx context.Context, user cq.CreateUserRequest) (*cq.CreateUserResponse, error)
}
type UserRepository interface {
io.Closer
UserReadRepository
UserWriteRepository
}

View file

@ -0,0 +1,143 @@
package services
import (
"bytes"
"crypto/rand"
"errors"
"fmt"
"strconv"
"golang.org/x/crypto/argon2"
"code.icb4dc0.de/prskr/searcherside/core/ports"
)
const (
argon2idSaltLength = 16
argon2idKeyLength uint32 = 32
)
var _ ports.PasswordHashAlgorithm = (*Argon2IDHashAlgorithm)(nil)
var ErrPasswordHashMismatch = errors.New("password hash mismatch")
type Argon2IDHashAlgorithm struct {
Argon2Params
}
func (a *Argon2IDHashAlgorithm) Hash(password []byte) ([]byte, error) {
params := a.Argon2Params
if params == (Argon2Params{}) {
params = DefaultArgon2Params
}
salt := make([]byte, argon2idSaltLength)
_, _ = rand.Read(salt)
pwh := &PasswordHash{
Hash: argon2.IDKey(password, salt, params.Iterations, params.Memory, params.Threads, argon2idKeyLength),
Salt: salt,
Params: &a.Argon2Params,
}
return pwh.MarshalText()
}
func (a *Argon2IDHashAlgorithm) Validate(password []byte, hash []byte) error {
var pwh PasswordHash
if err := pwh.UnmarshalText(hash); err != nil {
return fmt.Errorf("failed parse hash: %w", err)
}
params, ok := pwh.Params.(*Argon2Params)
if !ok {
return fmt.Errorf("hash params type mismatch: %T", pwh.Params)
}
actual := argon2.IDKey(password, pwh.Salt, params.Iterations, params.Memory, params.Threads, argon2idKeyLength)
if !bytes.Equal(actual, pwh.Hash) {
return ErrPasswordHashMismatch
}
return nil
}
var (
_ hashParams = (*Argon2Params)(nil)
)
var DefaultArgon2Params = Argon2Params{
Iterations: 3,
Memory: 64 * 1024,
Threads: 2,
}
type Argon2Params struct {
Iterations uint32
Memory uint32
Threads uint8
}
func (*Argon2Params) Algorithm() string {
return "argon2id"
}
// MarshalText implements encoding.TextMarshaler.
func (p *Argon2Params) MarshalText() (text []byte, err error) {
if p == nil || *p == (Argon2Params{}) {
return DefaultArgon2Params.MarshalText()
}
return []byte(fmt.Sprintf("v=19$m=%d,t=%d,p=%d", p.Memory, p.Iterations, p.Threads)), nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (p *Argon2Params) UnmarshalText(text []byte) error {
parts := bytes.Split(text, []byte(","))
if len(parts) < 3 {
return fmt.Errorf("invalid argon2 parameter: %s", text)
}
for i := range parts {
keyValue := bytes.Split(parts[i], []byte("="))
if len(keyValue) != 2 {
return fmt.Errorf("invalid argon2 parameter: %s", parts[i])
}
switch string(keyValue[0]) {
case "v":
continue
case "m":
parsed, err := strconv.ParseUint(string(keyValue[1]), 10, 32)
if err != nil {
return fmt.Errorf("failed to parse argon2 memory value %s: %w", keyValue[1], err)
}
p.Memory = uint32(parsed)
case "t":
parsed, err := strconv.ParseUint(string(keyValue[1]), 10, 32)
if err != nil {
return fmt.Errorf("failed to parse argon2 iteration value %s: %w", keyValue[1], err)
}
p.Iterations = uint32(parsed)
case "p":
parsed, err := strconv.ParseUint(string(keyValue[1]), 10, 8)
if err != nil {
return fmt.Errorf("failed to parse argon2 threads value %s: %w", keyValue[1], err)
}
p.Threads = uint8(parsed)
default:
return fmt.Errorf("unexpected parameter key: %s", keyValue[0])
}
}
return nil
}
func paramsForAlgorithm(alg string) hashParams {
switch alg {
case "argon2id":
return new(Argon2Params)
default:
return nil
}
}

View file

@ -0,0 +1,86 @@
package services_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"code.icb4dc0.de/prskr/searcherside/core/services"
)
func TestArgon2IDHashAlgorithm_Hash(t *testing.T) {
type fields struct {
Argon2Params services.Argon2Params
}
type args struct {
password []byte
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "Basic test",
fields: fields{
Argon2Params: services.DefaultArgon2Params,
},
args: args{
password: []byte("hello_world1234!"),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &services.Argon2IDHashAlgorithm{
Argon2Params: tt.fields.Argon2Params,
}
got, err := a.Hash(tt.args.password)
if (err != nil) != tt.wantErr {
t.Errorf("Hash() error = %v, wantErr %v", err, tt.wantErr)
return
}
t.Log(string(got))
var pwh services.PasswordHash
assert.NoError(t, pwh.UnmarshalText(got))
assert.NotEmpty(t, pwh.Hash)
assert.NotEmpty(t, pwh.Salt)
assert.NotZero(t, pwh.Params)
})
}
}
func TestArgon2IDHashAlgorithm_Validate(t *testing.T) {
type args struct {
password []byte
hash []byte
}
tests := []struct {
name string
args args
wantErr assert.ErrorAssertionFunc
}{
{
name: "Happy path",
args: args{
hash: []byte("$argon2id$v=19$m=65536,t=3,p=2$Huar9oxLB/FrNHbY2EldpA$NfSo9dk+YEJ8AewO6y1GHoNyW8HBXo4CDdwQNXmeq4g"),
password: []byte("hello_world1234!"),
},
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
return assert.NoError(t, err, i)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var a services.Argon2IDHashAlgorithm
tt.wantErr(t, a.Validate(tt.args.password, tt.args.hash), fmt.Sprintf("Validate(%v, %v)", tt.args.password, tt.args.hash))
})
}
}

View file

@ -12,7 +12,7 @@ import (
"github.com/blevesearch/bleve/v2/mapping"
"code.icb4dc0.de/prskr/searcherside/core/ports"
"code.icb4dc0.de/prskr/searcherside/internal/archive"
"code.icb4dc0.de/prskr/searcherside/infrastructure/archive"
)
var _ ports.Indexer = (*BleveIndexer)(nil)

View file

@ -0,0 +1,81 @@
package services
import (
"bytes"
"encoding"
"encoding/base64"
"errors"
)
var (
_ encoding.TextMarshaler = (*PasswordHash)(nil)
_ encoding.TextUnmarshaler = (*PasswordHash)(nil)
)
var ErrPasswordHashFormat = errors.New("invalid password hash format")
type hashParams interface {
Algorithm() string
encoding.TextMarshaler
encoding.TextUnmarshaler
}
type PasswordHash struct {
Hash []byte
Salt []byte
Params hashParams
}
func (p *PasswordHash) MarshalText() (text []byte, err error) {
buf := bytes.NewBuffer(make([]byte, 0, 256))
buf.WriteRune('$')
buf.WriteString(p.Params.Algorithm())
buf.WriteRune('$')
params, err := p.Params.MarshalText()
if err != nil {
return nil, err
}
buf.Write(params)
buf.WriteRune('$')
encodedSalt := make([]byte, base64.RawStdEncoding.EncodedLen(len(p.Salt)))
base64.RawStdEncoding.Encode(encodedSalt, p.Salt)
buf.Write(encodedSalt)
buf.WriteRune('$')
encodedCipherText := make([]byte, base64.RawStdEncoding.EncodedLen(len(p.Hash)))
base64.RawStdEncoding.Encode(encodedCipherText, p.Hash)
buf.Write(encodedCipherText)
return buf.Bytes(), nil
}
func (p *PasswordHash) UnmarshalText(text []byte) error {
split := bytes.Split(text, []byte("$"))
if len(split) < 6 {
return ErrPasswordHashFormat
}
p.Params = paramsForAlgorithm(string(split[1]))
if err := p.Params.UnmarshalText(split[len(split)-3]); err != nil {
return err
}
salt := split[len(split)-2]
p.Salt = make([]byte, base64.RawStdEncoding.DecodedLen(len(salt)))
if _, err := base64.RawStdEncoding.Decode(p.Salt, salt); err != nil {
return err
}
hash := split[len(split)-1]
p.Hash = make([]byte, base64.RawStdEncoding.DecodedLen(len(hash)))
if _, err := base64.RawStdEncoding.Decode(p.Hash, hash); err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,34 @@
package services_test
import (
"testing"
"code.icb4dc0.de/prskr/searcherside/core/services"
)
func TestPasswordHash_UnmarshalText(t *testing.T) {
type args struct {
text []byte
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Basic test",
args: args{
text: []byte("$argon2id$v=19$m=65536,t=3,p=2$RiKQX+VDY9FtotJETFq8WQ$uCWUzPOlKpIinzryBfy31IHjhn5miKvtS5hER5H+1RE"),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &services.PasswordHash{}
if err := p.UnmarshalText(tt.args.text); (err != nil) != tt.wantErr {
t.Errorf("UnmarshalText() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View file

@ -10,7 +10,7 @@ import (
"github.com/klauspost/compress/zstd"
"code.icb4dc0.de/prskr/searcherside/core/ports"
"code.icb4dc0.de/prskr/searcherside/internal/archive"
"code.icb4dc0.de/prskr/searcherside/infrastructure/archive"
)
var _ ports.Archiver = (*TarZSTIndexArchiver)(nil)

5
generate.go Normal file
View file

@ -0,0 +1,5 @@
package main
//go:generate go run -mod=mod github.com/a-h/templ/cmd/templ generate
//go:generate go run -mod=mod ./infrastructure/db/entc.go
//go:generate go run -mod=mod github.com/99designs/gqlgen

95
go.mod
View file

@ -5,19 +5,40 @@ go 1.22
toolchain go1.22.4
require (
ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43
entgo.io/contrib v0.5.0
entgo.io/ent v0.13.1
github.com/99designs/gqlgen v0.17.48
github.com/a-h/templ v0.2.707
github.com/alecthomas/kong v0.9.0
github.com/blevesearch/bleve/v2 v2.4.0
github.com/go-chi/chi/v5 v5.0.12
github.com/go-chi/cors v1.2.1
github.com/go-chi/jwtauth/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/hashicorp/go-multierror v1.1.1
github.com/jackc/pgx/v5 v5.6.0
github.com/klauspost/compress v1.17.8
github.com/lestrrat-go/jwx/v2 v2.0.21
github.com/mitchellh/mapstructure v1.5.0
github.com/stretchr/testify v1.9.0
github.com/testcontainers/testcontainers-go v0.31.0
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0
github.com/vektah/gqlparser/v2 v2.5.12
golang.org/x/crypto v0.24.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.30.1
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/Microsoft/hcsshim v0.11.4 // indirect
github.com/RoaringBitmap/roaring v1.9.4 // indirect
github.com/agext/levenshtein v1.2.1 // indirect
github.com/agnivade/levenshtein v1.1.1 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/blevesearch/bleve_index_api v1.1.9 // indirect
github.com/blevesearch/geo v0.1.20 // indirect
@ -36,25 +57,93 @@ require (
github.com/blevesearch/zapx/v14 v14.3.10 // indirect
github.com/blevesearch/zapx/v15 v15.3.13 // indirect
github.com/blevesearch/zapx/v16 v16.1.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/containerd/containerd v1.7.15 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/dockercfg v0.3.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect
github.com/distribution/reference v0.5.0 // indirect
github.com/docker/docker v25.0.5+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl/v2 v2.13.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.5 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.5.0 // indirect
github.com/moby/sys/user v0.1.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shirou/gopsutil/v3 v3.23.12 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/urfave/cli/v2 v2.27.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.9 // indirect
github.com/vmihailenco/tagparser v0.1.2 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
github.com/zclconf/go-cty v1.8.0 // indirect
go.etcd.io/bbolt v1.3.10 // indirect
golang.org/x/crypto v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d // indirect
google.golang.org/grpc v1.58.3 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.52.1 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)

315
go.sum
View file

@ -1,11 +1,47 @@
ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43 h1:GwdJbXydHCYPedeeLt4x/lrlIISQ4JTH1mRWuE5ZZ14=
ariga.io/atlas v0.19.1-0.20240203083654-5948b60a8e43/go.mod h1:uj3pm+hUTVN/X5yfdBexHlZv+1Xu5u5ZbZx7+CDavNU=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
entgo.io/contrib v0.5.0 h1:M4IqodImfUm327RDwNAITLNz3PsxVeC3rD4DPeVA8Gs=
entgo.io/contrib v0.5.0/go.mod h1:q8dXQCmzqpSlVdT2bWDydjgznGcy3y4zmsYmVFC9V/U=
entgo.io/ent v0.13.1 h1:uD8QwN1h6SNphdCCzmkMN3feSUzNnVvV/WIkHKMbzOE=
entgo.io/ent v0.13.1/go.mod h1:qCEmo+biw3ccBn9OyL4ZK5dfpwg++l1Gxwac5B1206A=
github.com/99designs/gqlgen v0.17.48 h1:Wgk7n9PIdnmpsC1aJJV4eiZCGkAkoamKOtXAp/crpzQ=
github.com/99designs/gqlgen v0.17.48/go.mod h1:hYeQ+ygPbcapbaHtHMbZ1DHMVNT+1tGU+fI+Hy4kqIo=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
github.com/a-h/templ v0.2.707 h1:T1Gkd2ugbRglZ9rYw/VBchWOSZVKmetDbBkm4YubM7U=
github.com/a-h/templ v0.2.707/go.mod h1:5cqsugkq9IerRNucNsI4DEamdHPsoGMQy99DzydLhM8=
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=
github.com/alecthomas/assert/v2 v2.6.0 h1:o3WJwILtexrEUk3cUVal3oiQY2tfgr/FHWiz/v2n4FU=
github.com/alecthomas/assert/v2 v2.6.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA=
github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
@ -45,39 +81,114 @@ github.com/blevesearch/zapx/v15 v15.3.13 h1:6EkfaZiPlAxqXz0neniq35my6S48QI94W/wy
github.com/blevesearch/zapx/v15 v15.3.13/go.mod h1:Turk/TNRKj9es7ZpKK95PS7f6D44Y7fAFy8F4LXQtGg=
github.com/blevesearch/zapx/v16 v16.1.1 h1:k+fDKs4ylqqw+X1PopzoxMbDdwgMOaXSbRCo0jnfR2Q=
github.com/blevesearch/zapx/v16 v16.1.1/go.mod h1:Zmq22YL64zvplIjUftIsYX3pV085F0wff8zukUZUww4=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/containerd/containerd v1.7.15 h1:afEHXdil9iAm03BmhjzKyXnnEBtjaLJefdU7DV0IFes=
github.com/containerd/containerd v1.7.15/go.mod h1:ISzRRTMF8EXNpJlTzyr2XMhN+j9K302C21/+cr3kUnY=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E=
github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0=
github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE=
github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A=
github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I=
github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc=
github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
@ -90,42 +201,228 @@ github.com/lestrrat-go/jwx/v2 v2.0.21 h1:jAPKupy4uHgrHFEdjVjNkUgoBKtVDgrQPB/h55F
github.com/lestrrat-go/jwx/v2 v2.0.21/go.mod h1:09mLW8zto6bWL9GbwnqAli+ArLf+5M33QLQPDggkUWM=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc=
github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo=
github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg=
github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/testcontainers/testcontainers-go v0.31.0 h1:W0VwIhcEVhRflwL9as3dhY6jXjVCA27AkmbnZ+UTh3U=
github.com/testcontainers/testcontainers-go v0.31.0/go.mod h1:D2lAoA0zUFiSY+eAflqK5mcUx/A5hrrORaEQrd0SefI=
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0 h1:isAwFS3KNKRbJMbWv+wolWqOFUECmjYZ+sIRZCIBc/E=
github.com/testcontainers/testcontainers-go/modules/postgres v0.31.0/go.mod h1:ZNYY8vumNCEG9YI59A9d6/YaMY49uwRhmeU563EzFGw=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
github.com/vektah/gqlparser/v2 v2.5.12 h1:COMhVVnql6RoaF7+aTBWiTADdpLGyZWU3K/NwW0ph98=
github.com/vektah/gqlparser/v2 v2.5.12/go.mod h1:WQQjFc+I1YIzoPvZBhUQX7waZgg3pMLi0r8KymvAE2w=
github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4=
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.9 h1:iBRIniTnWOo0kqkg3k3XR8Vn6OCkVlIuZNo0UoBrKx4=
github.com/vmihailenco/msgpack/v5 v5.0.0-beta.9/go.mod h1:HVxBVPUK/+fZMonk4bi1islLa8V3cfnBug0+4dykPzo=
github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc=
github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA=
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o=
go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w=
golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 h1:Z0hjGZePRE0ZBWotvtrwxFNrNE9CUAGtplaDK5NNI/g=
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw=
google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d h1:pgIUhmqwKOUlnKna4r6amKdUngdL8DrkpFeV8+VBElY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230731190214-cbb8c96f2d6d/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk=
modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.17.10 h1:6wrtRozgrhCxieCeJh85QsxkX/2FFrT9hdaWPlbn4Zo=
modernc.org/ccgo/v4 v4.17.10/go.mod h1:0NBHgsqTTpm9cA5z2ccErvGZmtntSM9qD2kFAs6pjXM=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.52.1 h1:uau0VoiT5hnR+SpoWekCKbLqm7v6dhRL3hI+NQhgN3M=
modernc.org/libc v1.52.1/go.mod h1:HR4nVzFDSDizP620zcMCgjb1/8xk2lg5p/8yjfGv1IQ=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.30.1 h1:YFhPVfu2iIgUf9kuA1CR7iiHdcEEsI2i+yjRYHscyxk=
modernc.org/sqlite v1.30.1/go.mod h1:DUmsiWQDaAvU4abhc/N+djlom/L2o8f7gZ95RCvyoLU=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

36
gqlgen.yml Normal file
View file

@ -0,0 +1,36 @@
schema:
- assets/api/*.graphql
autobind:
- code.icb4dc0.de/prskr/searcherside/internal/ent
- code.icb4dc0.de/prskr/searcherside/internal/ent/site
exec:
layout: follow-schema
dir: infrastructure/api/graphql/generated
package: generated
resolver:
layout: follow-schema
dir: handlers/graphql/
# Disable the generated getters for all models and interfaces.
omit_getters: true
models:
UUID:
model:
- github.com/99designs/gqlgen/graphql.ID
- github.com/99designs/gqlgen/graphql.Int
- github.com/99designs/gqlgen/graphql.Int64
- github.com/99designs/gqlgen/graphql.Int32
- github.com/99designs/gqlgen/graphql.UUID
ID:
model:
- code.icb4dc0.de/prskr/searcherside/infrastructure/db/schema/uuidgql.UUID
Node:
model:
- code.icb4dc0.de/prskr/searcherside/internal/ent.Noder
AccessLevel:
model:
- code.icb4dc0.de/prskr/searcherside/core/domain.AccessLevel

View file

@ -11,7 +11,7 @@ import (
"github.com/go-chi/chi/v5"
"code.icb4dc0.de/prskr/searcherside/core/ports"
"code.icb4dc0.de/prskr/searcherside/internal/logging"
logging2 "code.icb4dc0.de/prskr/searcherside/infrastructure/logging"
)
type IndexHandler struct {
@ -20,10 +20,10 @@ type IndexHandler struct {
}
func (h IndexHandler) IngestIndex(writer http.ResponseWriter, req *http.Request) {
logger := logging.GetLogger(req.Context())
logger := logging2.GetLogger(req.Context())
if err := req.ParseMultipartForm(h.MaxMemoryBytes); err != nil {
logger.WarnContext(req.Context(), "Failed to parse multipart form", logging.Error(err))
logger.WarnContext(req.Context(), "Failed to parse multipart form", logging2.Error(err))
http.Error(writer, "Failed to parse multipart form", http.StatusInternalServerError)
return
}
@ -47,7 +47,7 @@ func (h IndexHandler) IngestIndex(writer http.ResponseWriter, req *http.Request)
indexTempFile, err := os.CreateTemp(os.TempDir(), "searcherside-index-*")
if err != nil {
logger.ErrorContext(req.Context(), "Failed to create temporary index file", logging.Error(err))
logger.ErrorContext(req.Context(), "Failed to create temporary index file", logging2.Error(err))
http.Error(writer, "Failed to create temporary index file", http.StatusInternalServerError)
return
}
@ -56,23 +56,23 @@ func (h IndexHandler) IngestIndex(writer http.ResponseWriter, req *http.Request)
stream, err := indexFile.Open()
if err != nil {
logger.ErrorContext(req.Context(), "Failed to open index file", logging.Error(err))
logger.ErrorContext(req.Context(), "Failed to open index file", logging2.Error(err))
http.Error(writer, "Failed to open index file", http.StatusBadRequest)
return
}
if _, err := io.Copy(io.MultiWriter(hash, indexTempFile), stream); err != nil {
logger.ErrorContext(req.Context(), "Failed to copy index file", logging.Error(err))
logger.ErrorContext(req.Context(), "Failed to copy index file", logging2.Error(err))
http.Error(writer, "Failed to copy index file", http.StatusBadRequest)
return
}
if err := stream.Close(); err != nil {
logger.ErrorContext(req.Context(), "Failed to close index file", logging.Error(err))
logger.ErrorContext(req.Context(), "Failed to close index file", logging2.Error(err))
}
if err := indexTempFile.Close(); err != nil {
logger.ErrorContext(req.Context(), "Failed to close temporary index file", logging.Error(err))
logger.ErrorContext(req.Context(), "Failed to close temporary index file", logging2.Error(err))
http.Error(writer, "Failed to close temporary index file", http.StatusInternalServerError)
return
}
@ -86,7 +86,7 @@ func (h IndexHandler) IngestIndex(writer http.ResponseWriter, req *http.Request)
})
if indexErr != nil {
logger.ErrorContext(req.Context(), "Failed to ingest index", logging.Error(indexErr))
logger.ErrorContext(req.Context(), "Failed to ingest index", logging2.Error(indexErr))
}
}()

View file

@ -3,6 +3,10 @@ package v1
import (
"net/http"
"code.icb4dc0.de/prskr/searcherside/handlers/graphql"
"code.icb4dc0.de/prskr/searcherside/internal/ent"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/99designs/gqlgen/graphql/playground"
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"github.com/go-chi/jwtauth/v5"
@ -14,7 +18,11 @@ func Mount(
authSecret []byte,
indexHandler IndexHandler,
searchHandler SearchHandler,
dbClient *ent.Client,
) {
graphQlServer := handler.NewDefaultServer(graphql.NewSchema(dbClient))
r.Mount("/graphql", graphQlServer)
r.Mount("/graphql/playground", playground.Handler("SearcherSide", "/api/v1/graphql"))
r.Group(func(r chi.Router) {
jwtAuth := jwtauth.New(jwa.HS256.String(), authSecret, nil)

View file

@ -9,7 +9,7 @@ import (
"github.com/go-chi/chi/v5"
"code.icb4dc0.de/prskr/searcherside/core/ports"
"code.icb4dc0.de/prskr/searcherside/internal/logging"
logging2 "code.icb4dc0.de/prskr/searcherside/infrastructure/logging"
)
type SearchHandler struct {
@ -17,7 +17,7 @@ type SearchHandler struct {
}
func (h SearchHandler) PreviewSearch(writer http.ResponseWriter, request *http.Request) {
logger := logging.GetLogger(request.Context())
logger := logging2.GetLogger(request.Context())
idxKey := ports.IndexKey{
Module: chi.URLParam(request, "module"),
@ -27,19 +27,19 @@ func (h SearchHandler) PreviewSearch(writer http.ResponseWriter, request *http.R
logger.Info("Get searcher for index", slog.String("module", idxKey.Module), slog.String("instance", idxKey.Instance))
searcher, err := h.Curator.Searcher(idxKey)
if err != nil {
logger.Error("Error getting searcher", logging.Error(err))
logger.Error("Error getting searcher", logging2.Error(err))
}
result, err := searcher.Search(request.Context(), searchRequestFrom(request))
if err != nil {
logger.Error("Failed to search", logging.Error(err))
logger.Error("Failed to search", logging2.Error(err))
http.Error(writer, "Failed to search", http.StatusInternalServerError)
}
writer.Header().Set("Content-Type", "application/json")
encoder := json.NewEncoder(writer)
if err := encoder.Encode(result); err != nil {
logger.Error("Failed to encode search result", logging.Error(err))
logger.Error("Failed to encode search result", logging2.Error(err))
http.Error(writer, "Failed to encode search result", http.StatusInternalServerError)
return
}

46
handlers/cli/migrate.go Normal file
View file

@ -0,0 +1,46 @@
package cli
import (
"context"
"errors"
"fmt"
"github.com/alecthomas/kong"
"code.icb4dc0.de/prskr/searcherside/core/ports"
"code.icb4dc0.de/prskr/searcherside/infrastructure/config"
"code.icb4dc0.de/prskr/searcherside/internal/ent"
)
type MigrateHandler struct {
DB config.DB `embed:"" prefix:"db."`
}
func (m *MigrateHandler) Run(ctx context.Context, migrator ports.Migrator, client *ent.Client) (err error) {
defer func() {
err = errors.Join(err, client.Close())
}()
req := ports.MigrationRequest{
Driver: m.DB.Driver,
URL: m.DB.URL,
}
if err := migrator.Migrate(ctx, req); err != nil {
return fmt.Errorf("failed to apply migrations: %w", err)
}
return nil
}
func (m *MigrateHandler) AfterApply(kctx *kong.Context) error {
migrator, client, err := m.DB.Migrator()
if err != nil {
return fmt.Errorf("failed to initialize migrator: %w", err)
}
kctx.Bind(client)
kctx.BindTo(migrator, (*ports.Migrator)(nil))
return nil
}

View file

@ -11,13 +11,17 @@ import (
"path/filepath"
"time"
"github.com/alecthomas/kong"
"github.com/go-chi/chi/v5"
"code.icb4dc0.de/prskr/searcherside/infrastructure/config"
logging2 "code.icb4dc0.de/prskr/searcherside/infrastructure/logging"
"code.icb4dc0.de/prskr/searcherside/internal/cli"
"code.icb4dc0.de/prskr/searcherside/internal/ent"
"code.icb4dc0.de/prskr/searcherside/core/services"
v1 "code.icb4dc0.de/prskr/searcherside/handlers/api/v1"
"code.icb4dc0.de/prskr/searcherside/infrastructure/api"
"code.icb4dc0.de/prskr/searcherside/internal/flags"
"code.icb4dc0.de/prskr/searcherside/internal/logging"
"code.icb4dc0.de/prskr/searcherside/infrastructure/api/middlewares"
)
const (
@ -33,19 +37,23 @@ type ServerHandler struct {
ParseMaxMemoryBytes int64 `env:"HTTP_PARSE_MAX_MEMORY_BYTES" name:"parse-max-memory-bytes" help:"Parse max memory bytes" default:"33554432"`
} `embed:"" prefix:"http."`
Auth struct {
JwtSecret flags.HexString `env:"AUTH_JWT_SECRET" name:"jwt-secret" help:"JWT secret"`
JwtSecret cli.HexString `env:"AUTH_JWT_SECRET" name:"jwt-secret" help:"JWT secret"`
} `embed:"" prefix:"auth."`
DB config.DB `embed:"" prefix:"db."`
}
func (h *ServerHandler) Run(ctx context.Context, logger *slog.Logger) error {
func (h *ServerHandler) Run(ctx context.Context, entClient *ent.Client, logger *slog.Logger) (err error) {
defer func() {
err = errors.Join(err, entClient.Close())
}()
indexCurator, err := services.NewFileIndexCurator(
filepath.Join(h.DataDirectory, "searcherside.json"),
services.BleveIndexer{DataDirectory: h.DataDirectory},
services.TarZSTIndexArchiver{DataDirectory: h.DataDirectory},
)
if err != nil {
logger.Error("Failed to create index curator", logging.Error(err))
logger.Error("Failed to create index curator", logging2.Error(err))
return err
}
@ -55,7 +63,7 @@ func (h *ServerHandler) Run(ctx context.Context, logger *slog.Logger) error {
}
r := chi.NewRouter()
r.Use(api.LoggingMiddleware)
r.Use(middlewares.LoggingMiddleware)
r.Route("/api/v1", func(r chi.Router) {
indexHandler := v1.IndexHandler{
@ -67,7 +75,7 @@ func (h *ServerHandler) Run(ctx context.Context, logger *slog.Logger) error {
Curator: indexCurator,
}
v1.Mount(r, secret, indexHandler, searchHandler)
v1.Mount(r, secret, indexHandler, searchHandler, entClient)
})
srv := http.Server{
@ -75,7 +83,7 @@ func (h *ServerHandler) Run(ctx context.Context, logger *slog.Logger) error {
Handler: r,
ReadHeaderTimeout: h.Config.ReadHeaderTimeout,
BaseContext: func(listener net.Listener) context.Context {
return logging.ContextWithLogger(ctx, logger)
return logging2.ContextWithLogger(ctx, logger)
},
}
@ -83,7 +91,7 @@ func (h *ServerHandler) Run(ctx context.Context, logger *slog.Logger) error {
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error("Failed to start server", logging.Error(err))
logger.Error("Failed to start server", logging2.Error(err))
}
}()
@ -91,13 +99,23 @@ func (h *ServerHandler) Run(ctx context.Context, logger *slog.Logger) error {
logger.Info("Shutting down server")
shutdownCtx, cancel := context.WithTimeout(context.Background(), h.Config.ShutDownTimeout)
if err := srv.Shutdown(shutdownCtx); err != nil {
logger.Error("Failed to shutdown server", logging.Error(err))
logger.Error("Failed to shutdown server", logging2.Error(err))
}
cancel()
return nil
}
func (h *ServerHandler) AfterApply(kctx *kong.Context) error {
if entClient, err := h.DB.Client(); err != nil {
return err
} else {
kctx.Bind(entClient)
}
return nil
}
func (h *ServerHandler) jwtSecret() ([]byte, error) {
if len(h.Auth.JwtSecret) == 0 {
h.Auth.JwtSecret = make([]byte, jwtSecretLength)

View file

@ -10,19 +10,17 @@ import (
"github.com/lestrrat-go/jwx/v2/jwt"
"code.icb4dc0.de/prskr/searcherside/core/ports"
"code.icb4dc0.de/prskr/searcherside/internal/flags"
"code.icb4dc0.de/prskr/searcherside/internal/cli"
)
var (
ErrJwtSecretRequired = errors.New("JWT secret is required")
)
var ErrJwtSecretRequired = errors.New("JWT secret is required")
type TokenHandler struct {
Token struct {
Secret flags.HexString `name:"secret" help:"JWT secret"`
Secret cli.HexString `name:"secret" help:"JWT secret"`
Lifetime time.Duration `name:"lifetime" help:"JWT lifetime" default:"24h"`
Subject string `name:"subject" help:"JWT subject" default:"${WHOAMI=nobody}"`
Claims []flags.TokenClaim `name:"claims" help:"JWT claims"`
Claims []cli.TokenClaim `name:"claims" help:"JWT claims"`
} `embed:"" prefix:"token."`
}

View file

@ -0,0 +1,49 @@
package cli
import (
"context"
"errors"
"github.com/alecthomas/kong"
"code.icb4dc0.de/prskr/searcherside/core/cq"
"code.icb4dc0.de/prskr/searcherside/core/ports"
"code.icb4dc0.de/prskr/searcherside/core/services"
"code.icb4dc0.de/prskr/searcherside/infrastructure/config"
"code.icb4dc0.de/prskr/searcherside/infrastructure/repository"
)
type CreateUserHandler struct {
Email string `arg:""`
Password []byte `arg:"" type:"password"`
Admin bool `name:"admin" help:"Should the user be an admin" default:"false"`
DB config.DB `embed:"" prefix:"db."`
}
func (h *CreateUserHandler) Run(ctx context.Context, userRepo ports.UserRepository) (err error) {
defer func() {
err = errors.Join(err, userRepo.Close())
}()
_, err = userRepo.CreateUser(ctx, cq.CreateUserRequest{
Email: h.Email,
Password: h.Password,
Admin: h.Admin,
})
return err
}
func (h *CreateUserHandler) AfterApply(kctx *kong.Context) error {
client, err := h.DB.Client()
if err != nil {
return err
}
kctx.BindTo(
repository.NewEntUserRepository(client, new(services.Argon2IDHashAlgorithm)),
(*ports.UserRepository)(nil),
)
return nil
}

23
handlers/cli/users.go Normal file
View file

@ -0,0 +1,23 @@
package cli
import (
"github.com/alecthomas/kong"
"code.icb4dc0.de/prskr/searcherside/infrastructure/config"
)
type UsersHandler struct {
Create CreateUserHandler `cmd:"" name:"create" help:"Create user"`
DB config.DB `embed:"" prefix:"db."`
}
func (h *UsersHandler) AfterApply(kctx *kong.Context) error {
if entClient, err := h.DB.Client(); err != nil {
return err
} else {
kctx.Bind(entClient)
}
return nil
}

View file

@ -0,0 +1,38 @@
package graphql
// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.48
import (
"context"
"code.icb4dc0.de/prskr/searcherside/infrastructure/api/graphql/generated"
"code.icb4dc0.de/prskr/searcherside/internal/ent"
"github.com/google/uuid"
)
// Node is the resolver for the node field.
func (r *queryResolver) Node(ctx context.Context, id uuid.UUID) (ent.Noder, error) {
return r.client.Noder(ctx, id)
}
// Nodes is the resolver for the nodes field.
func (r *queryResolver) Nodes(ctx context.Context, ids []uuid.UUID) ([]ent.Noder, error) {
return r.client.Noders(ctx, ids)
}
// Sites is the resolver for the sites field.
func (r *queryResolver) Sites(ctx context.Context) ([]*ent.Site, error) {
return r.client.Site.Query().All(ctx)
}
// Users is the resolver for the users field.
func (r *queryResolver) Users(ctx context.Context) ([]*ent.User, error) {
return r.client.User.Query().All(ctx)
}
// Query returns generated.QueryResolver implementation.
func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} }
type queryResolver struct{ *Resolver }

View file

@ -0,0 +1,20 @@
package graphql
import (
"code.icb4dc0.de/prskr/searcherside/infrastructure/api/graphql/generated"
"code.icb4dc0.de/prskr/searcherside/internal/ent"
"github.com/99designs/gqlgen/graphql"
)
// NewSchema creates a graphql executable schema.
func NewSchema(client *ent.Client) graphql.ExecutableSchema {
return generated.NewExecutableSchema(generated.Config{
Resolvers: &Resolver{
client: client,
},
})
}
type Resolver struct {
client *ent.Client
}

View file

@ -1,4 +1,4 @@
package api
package middlewares
import (
"encoding/hex"
@ -8,7 +8,7 @@ import (
"strconv"
"time"
"code.icb4dc0.de/prskr/searcherside/internal/logging"
"code.icb4dc0.de/prskr/searcherside/infrastructure/logging"
)
func LoggingMiddleware(next http.Handler) http.Handler {

View file

@ -0,0 +1,29 @@
package config
import (
"code.icb4dc0.de/prskr/searcherside/core/ports"
"code.icb4dc0.de/prskr/searcherside/infrastructure/db/migrate"
"code.icb4dc0.de/prskr/searcherside/internal/ent"
"code.icb4dc0.de/prskr/searcherside/scripts/migrations"
)
type DB struct {
Driver ports.Driver `env:"DB_DRIVER" name:"driver" default:"sqlite" help:"DB driver, either postgres or sqlite"`
URL string `env:"DB_URL" name:"url" default:"sqlite://file::memory:?cache=shared" help:"Connection URL"`
}
func (d DB) Client() (*ent.Client, error) {
return ent.Open(d.Driver.String(), d.URL)
}
func (d DB) Migrator() (ports.Migrator, *ent.Client, error) {
client, err := d.Client()
if err != nil {
return nil, nil, err
}
return migrate.AtlasMigrator{
MigrationsFS: migrations.Fs,
RevisionRW: migrate.NewDBRevisionRW(client),
}, client, nil
}

View file

@ -0,0 +1,17 @@
package config
import (
"log/slog"
)
type Logging struct {
AddSource bool `env:"LOG_ADD_SOURCE" name:"add-source" default:"false"`
Level slog.Level `env:"LOG_LEVEL" name:"level" default:"info" help:"Log level to apply"`
}
func (l Logging) Options() *slog.HandlerOptions {
return &slog.HandlerOptions{
Level: l.Level,
AddSource: l.AddSource,
}
}

View file

@ -56,15 +56,9 @@ func (y Yaml) Loader(r io.Reader) (kong.Resolver, error) {
}
return kong.ResolverFunc(func(context *kong.Context, parent *kong.Path, flag *kong.Flag) (any, error) {
path := strings.Split(flag.Name, y.separator())
if n := parent.Node(); n != nil && n.Type != kong.ApplicationNode {
for _, prefix := range append(n.Aliases, n.Name) {
if val := lookup(config, y.normalize(append([]string{prefix}, path...))); val != nil {
if val := lookup(config, y.normalize(strings.Split(flag.Name, y.separator()))); val != nil {
return val, nil
}
}
}
return nil, nil
}), nil

44
infrastructure/db/entc.go Normal file
View file

@ -0,0 +1,44 @@
//go:build ignore
package main
import (
"log"
"path/filepath"
"entgo.io/contrib/entgql"
"entgo.io/ent/entc"
"entgo.io/ent/entc/gen"
)
func main() {
ex, err := entgql.NewExtension(
entgql.WithWhereInputs(true),
// Tell Ent to generate a GraphQL schema for
// the Ent schema in a file named ent.graphql.
entgql.WithSchemaGenerator(),
entgql.WithSchemaPath(filepath.Join("assets", "api", "ent.graphql")),
entgql.WithConfigPath("gqlgen.yml"),
)
if err != nil {
log.Fatalf("creating entgql extension: %v", err)
}
opts := []entc.Option{
entc.Extensions(ex),
}
generatorConfig := &gen.Config{
Target: filepath.Join("internal", "ent"),
Package: "code.icb4dc0.de/prskr/searcherside/internal/ent",
Features: []gen.Feature{
gen.FeatureVersionedMigration,
gen.FeatureUpsert,
gen.FeaturePrivacy,
gen.FeatureEntQL,
},
}
if err := entc.Generate("./infrastructure/db/schema", generatorConfig, opts...); err != nil {
log.Fatalf("running ent codegen: %v", err)
}
}

View file

@ -0,0 +1,80 @@
package migrate
import (
"context"
"database/sql"
"errors"
"fmt"
"io/fs"
"code.icb4dc0.de/prskr/searcherside/core/ports"
"ariga.io/atlas/sql/migrate"
"ariga.io/atlas/sql/postgres"
"ariga.io/atlas/sql/sqlite"
)
var _ ports.Migrator = (*AtlasMigrator)(nil)
type AtlasMigrator struct {
MigrationsFS fs.FS
RevisionRW ports.RevisionReadWriter
}
func (a AtlasMigrator) Migrate(ctx context.Context, req ports.MigrationRequest) (err error) {
dialectFS, err := fs.Sub(a.MigrationsFS, req.Driver.String())
if err != nil {
return fmt.Errorf("no migrations sub-directory found for dialect %s: %w", req.Driver, err)
}
migrateDriver, conn, err := migrationDriverFor(req.Driver, req.URL)
if err != nil {
return err
}
defer func() {
err = errors.Join(err, conn.Close())
}()
executor, err := migrate.NewExecutor(migrateDriver, readOnlyFSDir{FS: dialectFS}, a.RevisionRW)
if err != nil {
return err
}
pendingFiles, err := executor.Pending(ctx)
if err != nil {
return err
}
for idx := range pendingFiles {
if err = executor.Execute(ctx, pendingFiles[idx]); err != nil {
return err
}
}
return nil
}
func migrationDriverFor(driverName ports.Driver, url string) (drv migrate.Driver, db *sql.DB, err error) {
conn, err := sql.Open(driverName.String(), url)
if err != nil {
return nil, nil, err
}
defer func() {
if err != nil {
err = errors.Join(err, conn.Close())
}
}()
switch driverName {
case ports.DriverSQLite:
drv, err = sqlite.Open(conn)
return drv, conn, err
case ports.DriverPostgres:
drv, err = postgres.Open(conn)
return drv, conn, err
default:
return nil, nil, fmt.Errorf("unknown driver: %s", driverName)
}
}

View file

@ -0,0 +1,71 @@
package migrate_test
import (
"context"
"fmt"
"path/filepath"
"testing"
"code.icb4dc0.de/prskr/searcherside/core/ports"
"code.icb4dc0.de/prskr/searcherside/infrastructure/db/migrate"
_ "code.icb4dc0.de/prskr/searcherside/internal/db"
"code.icb4dc0.de/prskr/searcherside/internal/ent"
"code.icb4dc0.de/prskr/searcherside/scripts/migrations"
"entgo.io/ent/dialect"
)
func TestAtlasMigrator_Migrate_SQLite(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "gophershare.db")
req := ports.MigrationRequest{
Driver: ports.DriverSQLite,
URL: fmt.Sprintf("file://%s?_pragma=foreign_keys(1)", dbPath),
}
client, err := ent.Open(dialect.SQLite, req.URL)
if err != nil {
t.Fatalf("failed to open sqlite3 client: %v", err)
}
migrator := migrate.AtlasMigrator{
MigrationsFS: migrations.Fs,
RevisionRW: migrate.NewDBRevisionRW(client),
}
if err := migrator.Migrate(context.Background(), req); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
}
func TestAtlasMigrator_Migrate_Postgres(t *testing.T) {
connString, closer, err := dbtest.TestPostgresDatabase(context.Background())
if err != nil {
t.Fatalf("failed to setup Postgres test DB: %v", err)
}
t.Cleanup(func() {
if err := closer.Close(context.Background()); err != nil {
t.Errorf("failed to close Postgres test DB: %v", err)
}
})
req := ports.MigrationRequest{
Driver: ports.DriverPostgres,
URL: connString,
}
client, err := ent.Open(dialect.Postgres, req.URL)
if err != nil {
t.Fatalf("failed to open sqlite3 client: %v", err)
}
migrator := migrate.AtlasMigrator{
MigrationsFS: migrations.Fs,
RevisionRW: migrate.NewDBRevisionRW(client),
}
if err = migrator.Migrate(context.Background(), req); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
}

View file

@ -0,0 +1,144 @@
package migrate
import (
"context"
"errors"
"time"
"code.icb4dc0.de/prskr/searcherside/core/ports"
"code.icb4dc0.de/prskr/searcherside/internal/ent"
"code.icb4dc0.de/prskr/searcherside/internal/ent/migration"
"ariga.io/atlas/sql/migrate"
"github.com/jackc/pgx/v5/pgconn"
"modernc.org/sqlite"
)
var _ ports.RevisionReadWriter = (*DBRevisionRW)(nil)
func NewDBRevisionRW(client *ent.Client) ports.RevisionReadWriter {
return &DBRevisionRW{
client: client,
}
}
type DBRevisionRW struct {
client *ent.Client
}
func (rw DBRevisionRW) Client() *ent.Client {
return rw.client
}
func (DBRevisionRW) Ident() *migrate.TableIdent {
return &migrate.TableIdent{
Name: migration.Table,
}
}
func (rw DBRevisionRW) ReadRevisions(ctx context.Context) ([]*migrate.Revision, error) {
allMigrations, err := rw.client.Migration.
Query().
All(ctx)
if err != nil && !ignoreError(err) {
return nil, err
}
revs := make([]*migrate.Revision, len(allMigrations))
for idx := range allMigrations {
revs[idx] = revisionOf(allMigrations[idx])
}
return revs, nil
}
func (rw DBRevisionRW) ReadRevision(ctx context.Context, s string) (*migrate.Revision, error) {
m, err := rw.client.Migration.
Query().
Where(migration.VersionEQ(s)).
Only(ctx)
if err != nil {
if ignoreError(err) {
return nil, migrate.ErrRevisionNotExist
}
return nil, err
}
return revisionOf(m), nil
}
func (rw DBRevisionRW) WriteRevision(ctx context.Context, revision *migrate.Revision) error {
err := rw.client.Migration.Create().
SetVersion(revision.Version).
SetDescription(revision.Description).
SetType(uint(revision.Type)).
SetApplied(revision.Applied).
SetTotal(revision.Total).
SetExecutedAt(revision.ExecutedAt).
SetExecutionTime(int64(revision.ExecutionTime)).
SetError(revision.Error).
SetErrorStmt(revision.ErrorStmt).
SetHash(revision.Hash).
SetPartialHashes(revision.PartialHashes).
SetOperatorVersion(revision.OperatorVersion).
OnConflictColumns(migration.FieldVersion).
UpdateNewValues().
Exec(ctx)
if ignoreError(err) {
return nil
}
return err
}
func (rw DBRevisionRW) DeleteRevision(ctx context.Context, s string) error {
_, err := rw.client.Migration.Delete().Where(migration.VersionEQ(s)).Exec(ctx)
return err
}
func revisionOf(m *ent.Migration) *migrate.Revision {
if m == nil {
return new(migrate.Revision)
}
return &migrate.Revision{
Version: m.Version,
Description: m.Description,
Type: migrate.RevisionType(m.Type),
Applied: m.Applied,
Total: m.Total,
ExecutedAt: m.ExecutedAt,
ExecutionTime: time.Duration(m.ExecutionTime),
Error: m.Error,
ErrorStmt: m.ErrorStmt,
Hash: m.Hash,
PartialHashes: m.PartialHashes,
OperatorVersion: m.OperatorVersion,
}
}
func ignoreError(err error) bool {
if ent.IsNotFound(err) {
return true
}
var postgresErr *pgconn.PgError
if errors.As(err, &postgresErr) {
switch postgresErr.Code {
case "42P01":
return true
}
}
// DB specific errors
var sqliteErr *sqlite.Error
if errors.As(err, &sqliteErr) {
if sqliteErr.Code() == 1 {
return true
}
}
return false
}

View file

@ -0,0 +1,79 @@
package migrate
import (
"errors"
"fmt"
"io"
"io/fs"
"path/filepath"
"ariga.io/atlas/sql/migrate"
)
var _ migrate.Dir = (*readOnlyFSDir)(nil)
type readOnlyFSDir struct {
fs.FS
}
func (m readOnlyFSDir) WriteFile(string, []byte) error {
return errors.New("migration directory is read-only")
}
func (m readOnlyFSDir) Files() ([]migrate.File, error) {
files, err := fs.ReadDir(m, ".")
if err != nil {
return nil, err
}
filesList := make([]migrate.File, 0, len(files))
for idx := range files {
f := files[idx]
if f.IsDir() || filepath.Ext(f.Name()) != ".sql" {
continue
}
scriptFile, err := m.Open(f.Name())
if err != nil {
return nil, fmt.Errorf("failed to open %s: %w", f.Name(), err)
}
defer func() {
_ = scriptFile.Close()
}()
scriptData, err := io.ReadAll(scriptFile)
if err != nil {
return nil, fmt.Errorf("failed to read %s: %w", f.Name(), err)
}
filesList = append(filesList, migrate.NewLocalFile(f.Name(), scriptData))
}
return filesList, nil
}
func (m readOnlyFSDir) Checksum() (migrate.HashFile, error) {
var hf migrate.HashFile
f, err := m.Open(migrate.HashFileName)
if err != nil {
return nil, err
}
defer func() {
_ = f.Close()
}()
sumData, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to %s: %w", migrate.HashFileName, err)
}
if err := hf.UnmarshalText(sumData); err != nil {
return nil, fmt.Errorf("failed to unmarshal %s: %w", migrate.HashFileName, err)
}
return hf, nil
}

View file

@ -0,0 +1,59 @@
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
"entgo.io/ent/schema/mixin"
"github.com/google/uuid"
)
type Index struct {
ent.Schema
}
func (Index) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.UUID{}).
Default(uuid.New).
Immutable().
Unique(),
field.UUID("site_id", uuid.UUID{}).
Default(uuid.New).
Immutable().
Unique(),
field.String("name").
NotEmpty(),
field.String("revision").
Immutable().
NotEmpty(),
}
}
func (Index) Edges() []ent.Edge {
return []ent.Edge{
edge.From("index", Site.Type).
Field("site_id").
Ref("indices").
Unique().
Immutable().
Required(),
}
}
func (Index) Indexes() []ent.Index {
return []ent.Index{
index.Fields("site_id", "name", "revision").
Unique(),
}
}
func (Index) Mixin() []ent.Mixin {
return []ent.Mixin{
mixin.Time{},
}
}

View file

@ -0,0 +1,62 @@
package schema
import (
"ariga.io/atlas/sql/migrate"
"entgo.io/contrib/entgql"
"entgo.io/ent"
"entgo.io/ent/schema"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/mixin"
)
type Migration struct {
ent.Schema
}
func (Migration) Fields() []ent.Field {
return []ent.Field{
field.String("version").
Unique().
Immutable(),
field.String("description").
Optional(),
field.Uint("type").
Default(uint(migrate.RevisionTypeUnknown)),
field.Int("applied").
Optional(),
field.Int("total").
Optional(),
field.Time("executed_at"),
field.Int64("execution_time"),
field.String("error").
Optional(),
field.String("error_stmt").
Optional(),
field.String("hash"),
field.Strings("partial_hashes"),
field.String("operator_version"),
}
}
func (Migration) Mixin() []ent.Mixin {
return []ent.Mixin{
mixin.Time{},
}
}
func (Migration) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.Skip(entgql.SkipAll),
}
}

View file

@ -0,0 +1,46 @@
package schema
import (
"entgo.io/contrib/entgql"
"entgo.io/ent"
"entgo.io/ent/schema"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/mixin"
"github.com/google/uuid"
)
type Site struct {
ent.Schema
}
func (Site) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.UUID{}).
Immutable().
Default(uuid.New),
field.String("name").
Immutable().
Unique(),
}
}
func (Site) Edges() []ent.Edge {
return []ent.Edge{
edge.To("site_members", UserStaff.Type),
edge.To("indices", Index.Type),
}
}
func (Site) Mixin() []ent.Mixin {
return []ent.Mixin{
mixin.Time{},
}
}
func (Site) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.QueryField(),
}
}

View file

@ -0,0 +1,69 @@
package schema
import (
"entgo.io/contrib/entgql"
"entgo.io/ent"
"entgo.io/ent/schema"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
"entgo.io/ent/schema/mixin"
"github.com/google/uuid"
)
type User struct {
ent.Schema
}
func (User) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.UUID{}).
Default(uuid.New).
Immutable().
Unique(),
field.String("email").
NotEmpty().
Immutable().
Unique(),
field.String("given_name").
Optional(),
field.String("surname").
Optional(),
field.Bool("is_admin").
Default(false).
Annotations(entgql.Skip(entgql.SkipAll)),
field.Bytes("password").
NotEmpty().
Sensitive().
Annotations(entgql.Skip(entgql.SkipAll)),
}
}
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("staffs", UserStaff.Type),
}
}
func (User) Indexes() []ent.Index {
return []ent.Index{
index.Fields("email"),
}
}
func (User) Mixin() []ent.Mixin {
return []ent.Mixin{
mixin.Time{},
}
}
func (User) Annotations() []schema.Annotation {
return []schema.Annotation{
entgql.QueryField(),
}
}

View file

@ -0,0 +1,52 @@
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/mixin"
"github.com/google/uuid"
)
type UserStaff struct {
ent.Schema
}
func (UserStaff) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.UUID{}).
Default(uuid.New).
Immutable().
Unique(),
field.UUID("user_id", uuid.UUID{}).
Immutable(),
field.UUID("site_id", uuid.UUID{}).
Immutable(),
}
}
func (UserStaff) Edges() []ent.Edge {
return []ent.Edge{
edge.From("users", User.Type).
Field("user_id").
Ref("staffs").
Unique().
Immutable().
Required(),
edge.From("site", Site.Type).
Field("site_id").
Ref("site_members").
Unique().
Immutable().
Required(),
}
}
func (UserStaff) Mixin() []ent.Mixin {
return []ent.Mixin{
mixin.Time{},
}
}

View file

@ -0,0 +1,24 @@
package uuidgql
import (
"fmt"
"io"
"strconv"
"github.com/99designs/gqlgen/graphql"
"github.com/google/uuid"
)
func MarshalUUID(u uuid.UUID) graphql.Marshaler {
return graphql.WriterFunc(func(w io.Writer) {
_, _ = io.WriteString(w, strconv.Quote(u.String()))
})
}
func UnmarshalUUID(v interface{}) (u uuid.UUID, err error) {
s, ok := v.(string)
if !ok {
return u, fmt.Errorf("invalid type %T, expect string", v)
}
return uuid.Parse(s)
}

View file

@ -0,0 +1,65 @@
package repository
import (
"context"
"fmt"
"github.com/google/uuid"
"code.icb4dc0.de/prskr/searcherside/core/cq"
"code.icb4dc0.de/prskr/searcherside/core/domain"
"code.icb4dc0.de/prskr/searcherside/core/ports"
"code.icb4dc0.de/prskr/searcherside/internal/ent"
"code.icb4dc0.de/prskr/searcherside/internal/shred"
)
var _ ports.UserRepository = (*EntUserRepository)(nil)
func NewEntUserRepository(client *ent.Client, hashAlgorithm ports.PasswordHashAlgorithm) *EntUserRepository {
return &EntUserRepository{
client: client,
hashAlgorithm: hashAlgorithm,
}
}
type EntUserRepository struct {
client *ent.Client
hashAlgorithm ports.PasswordHashAlgorithm
}
func (e EntUserRepository) Close() error {
return e.client.Close()
}
func (e EntUserRepository) CreateUser(ctx context.Context, user cq.CreateUserRequest) (*cq.CreateUserResponse, error) {
defer func() {
shred.Bytes(user.Password)
}()
hashedPassword, err := e.hashAlgorithm.Hash(user.Password)
if err != nil {
return nil, fmt.Errorf("failed to hash password: %w", err)
}
_, err = e.client.User.Create().
SetEmail(user.Email).
SetPassword(hashedPassword).
SetIsAdmin(user.Admin).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return new(cq.CreateUserResponse), nil
}
func (e EntUserRepository) UserByID(ctx context.Context, id uuid.UUID) (*domain.User, error) {
//TODO implement me
panic("implement me")
}
func (e EntUserRepository) UserByEmail(ctx context.Context, email string) (*domain.User, error) {
//TODO implement me
panic("implement me")
}

View file

@ -12,14 +12,15 @@ import (
"code.icb4dc0.de/prskr/searcherside/core/ports"
clih "code.icb4dc0.de/prskr/searcherside/handlers/cli"
"code.icb4dc0.de/prskr/searcherside/infrastructure/config"
"code.icb4dc0.de/prskr/searcherside/internal/cli"
)
type App struct {
Logging struct {
Level slog.Level `env:"LOG_LEVEL" help:"Log level" default:"warn"`
} `embed:"" prefix:"logging."`
Logging config.Logging `embed:"" prefix:"logging."`
Serve clih.ServerHandler `cmd:"" name:"serve" help:"Start the server" aliases:"server"`
Migrate clih.MigrateHandler `cmd:"" name:"migrate" help:"Apply database migrations"`
User clih.UsersHandler `cmd:"" name:"user" help:"Manage users"`
Token clih.TokenHandler `cmd:"" name:"token" help:"Generate a token"`
}
@ -40,6 +41,7 @@ func (a *App) Execute() error {
a,
kong.Name("searcherside"),
kong.Description("SearcherSide"),
kong.NamedMapper("password", cli.PasswordMapper{}),
kong.Bind(ports.CWD(wd)),
kong.BindTo(os.Stdout, (*ports.STDOUT)(nil)),
kong.BindTo(ctx, (*context.Context)(nil)),
@ -54,11 +56,7 @@ func (a *App) Execute() error {
}
func (a *App) AfterApply(kongCtx *kong.Context) error {
loggingOpts := slog.HandlerOptions{
Level: a.Logging.Level,
}
defaultLogger := slog.New(slog.NewJSONHandler(os.Stderr, &loggingOpts))
defaultLogger := slog.New(slog.NewJSONHandler(os.Stderr, a.Logging.Options()))
slog.SetDefault(defaultLogger)

View file

@ -1,4 +1,4 @@
package flags
package cli
import (
"fmt"

View file

@ -1,4 +1,4 @@
package flags
package cli
import (
"encoding/hex"

View file

@ -0,0 +1,43 @@
package cli
import (
"bufio"
"fmt"
"os"
"reflect"
"github.com/alecthomas/kong"
)
var _ kong.Mapper = (*PasswordMapper)(nil)
type PasswordMapper struct {
}
func (p PasswordMapper) Decode(ctx *kong.DecodeContext, target reflect.Value) error {
token := ctx.Scan.Pop()
switch v := token.Value.(type) {
case []byte:
target.SetBytes(v)
case string:
if len(v) == 0 {
return nil
}
if v == "-" {
scanner := bufio.NewScanner(os.Stdin)
if scanner.Scan() {
target.SetBytes(scanner.Bytes())
return nil
} else {
return fmt.Errorf("failed to read password from stdin: %v", scanner.Err())
}
}
target.SetBytes([]byte(v))
default:
return fmt.Errorf("expected bool but got %q (%T)", token.Value, token.Value)
}
return nil
}

View file

@ -0,0 +1,15 @@
package dbtest
import "context"
type DbCloser interface {
Close(ctx context.Context) error
}
type NoOpCloser func(context.Context) error
func (NoOpCloser) Close(context.Context) error { return nil }
type CloserFunc func(ctx context.Context) error
func (f CloserFunc) Close(ctx context.Context) error { return f(ctx) }

View file

@ -0,0 +1,19 @@
package dbtest
import (
"context"
"fmt"
"entgo.io/ent/dialect"
)
func TestDatabaseForDialect(ctx context.Context, dbDialect string) (string, DbCloser, error) {
switch dbDialect {
case dialect.Postgres:
return TestPostgresDatabase(ctx)
case dialect.SQLite:
return TestSQLiteDB(), NoOpCloser(nil), nil
default:
return "", nil, fmt.Errorf("unsupported dialect: %s", dbDialect)
}
}

View file

@ -0,0 +1,39 @@
package dbtest
import (
"context"
"github.com/testcontainers/testcontainers-go"
tcpostgres "github.com/testcontainers/testcontainers-go/modules/postgres"
"github.com/testcontainers/testcontainers-go/wait"
)
func TestPostgresDatabase(ctx context.Context) (string, DbCloser, error) {
container, err := tcpostgres.RunContainer(
ctx,
withImage("docker.io/postgres:16-alpine"),
tcpostgres.WithDatabase("gophershare"),
tcpostgres.WithUsername("postgres"),
tcpostgres.WithPassword("postgres"),
testcontainers.WithWaitStrategy(
wait.ForListeningPort("5432/tcp"),
),
)
if err != nil {
return "", nil, err
}
connString, err := container.ConnectionString(ctx, "sslmode=disable")
if err != nil {
return "", nil, err
}
return connString, CloserFunc(container.Terminate), nil
}
func withImage(image string) testcontainers.CustomizeRequestOption {
return func(req *testcontainers.GenericContainerRequest) error {
req.Image = image
return nil
}
}

View file

@ -0,0 +1,5 @@
package dbtest
func TestSQLiteDB() (connString string) {
return "sqlite://dev?mode=memory"
}

13
internal/db/init.go Normal file
View file

@ -0,0 +1,13 @@
package db
import (
"database/sql"
"github.com/jackc/pgx/v5/stdlib"
"modernc.org/sqlite"
)
func init() {
sql.Register("sqlite3", Sqlite3DriverWrapper{Driver: new(sqlite.Driver)})
sql.Register("postgres", PgxDriverWrapper{Driver: new(stdlib.Driver)})
}

View file

@ -0,0 +1,15 @@
package db
import (
"database/sql/driver"
"github.com/jackc/pgx/v5/stdlib"
)
type PgxDriverWrapper struct {
*stdlib.Driver
}
func (d PgxDriverWrapper) Open(name string) (conn driver.Conn, err error) {
return d.Driver.Open(name)
}

View file

@ -0,0 +1,27 @@
package db
import (
"database/sql/driver"
"modernc.org/sqlite"
)
type sqlite3DriverConn interface {
Exec(string, []driver.Value) (driver.Result, error)
}
type Sqlite3DriverWrapper struct {
*sqlite.Driver
}
func (d Sqlite3DriverWrapper) Open(name string) (conn driver.Conn, err error) {
conn, err = d.Driver.Open(name)
if err != nil {
return
}
_, err = conn.(sqlite3DriverConn).Exec("PRAGMA foreign_keys = ON;", nil)
if err != nil {
_ = conn.Close()
}
return
}

View file

@ -0,0 +1,77 @@
package main
import (
"context"
"errors"
"log"
"os"
"os/signal"
"path/filepath"
"time"
"code.icb4dc0.de/prskr/searcherside/internal/db/dbtest"
"code.icb4dc0.de/prskr/searcherside/internal/ent/migrate"
atlas "ariga.io/atlas/sql/migrate"
"entgo.io/ent/dialect"
"entgo.io/ent/dialect/sql/schema"
_ "code.icb4dc0.de/prskr/searcherside/internal/db"
)
var (
dir = filepath.Join("scripts", "migrations")
targets = []string{dialect.SQLite, dialect.Postgres}
)
func main() {
if len(os.Args) != 2 {
log.Fatalln("migration name is required. Use: 'go run -mod=mod code.icb4dc0.de/prskr/searcherside/internal/migrations <name>'")
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
for _, d := range targets {
if err := generateMigrations(ctx, d, os.Args[1]); err != nil {
cancel()
log.Fatalf("failed to generate migrations: %v", err)
}
}
cancel()
}
func generateMigrations(ctx context.Context, dbDialect, name string) (err error) {
migrationsDir := filepath.Join(dir, dbDialect)
if err := os.MkdirAll(migrationsDir, 0o755); err != nil {
return err
}
dir, err := atlas.NewLocalDir(migrationsDir)
if err != nil {
return err
}
opts := []schema.MigrateOption{
schema.WithDir(dir),
schema.WithMigrationMode(schema.ModeReplay),
schema.WithDialect(dbDialect),
schema.WithFormatter(atlas.DefaultFormatter),
schema.WithDropColumn(true),
schema.WithDropIndex(true),
}
connectionString, closer, err := dbtest.TestDatabaseForDialect(ctx, dbDialect)
if err != nil {
return err
}
defer func() {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
err = errors.Join(err, closer.Close(shutdownCtx))
cancel()
}()
return migrate.NamedDiff(ctx, connectionString, name, opts...)
}

7
internal/shred/shred.go Normal file
View file

@ -0,0 +1,7 @@
package shred
func Bytes(b []byte) {
for i := range b {
b[i] = 0
}
}

View file

@ -5,6 +5,7 @@ import (
"os"
"code.icb4dc0.de/prskr/searcherside/internal"
_ "code.icb4dc0.de/prskr/searcherside/internal/db"
)
func main() {

6
scripts/migrations/fs.go Normal file
View file

@ -0,0 +1,6 @@
package migrations
import "embed"
//go:embed postgres/* sqlite3/*
var Fs embed.FS

View file

@ -0,0 +1,20 @@
-- Create "migrations" table
CREATE TABLE "migrations"
(
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"create_time" timestamptz NOT NULL,
"update_time" timestamptz NOT NULL,
"version" character varying NOT NULL UNIQUE,
"description" character varying NULL,
"type" bigint NOT NULL DEFAULT 0,
"applied" bigint NULL,
"total" bigint NULL,
"executed_at" timestamptz NOT NULL,
"execution_time" bigint NOT NULL,
"error" character varying NULL,
"error_stmt" character varying NULL,
"hash" character varying NOT NULL,
"partial_hashes" jsonb NOT NULL,
"operator_version" character varying NOT NULL,
PRIMARY KEY ("id")
);

View file

@ -0,0 +1,16 @@
-- Create "sites" table
CREATE TABLE "sites" ("id" uuid NOT NULL, "create_time" timestamptz NOT NULL, "update_time" timestamptz NOT NULL, "name" character varying NOT NULL, PRIMARY KEY ("id"));
-- Create index "sites_name_key" to table: "sites"
CREATE UNIQUE INDEX "sites_name_key" ON "sites" ("name");
-- Create "indexes" table
CREATE TABLE "indexes" ("id" uuid NOT NULL, "create_time" timestamptz NOT NULL, "update_time" timestamptz NOT NULL, "name" character varying NOT NULL, "revision" character varying NOT NULL, "site_id" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "indexes_sites_indices" FOREIGN KEY ("site_id") REFERENCES "sites" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION);
-- Create index "index_site_id_name_revision" to table: "indexes"
CREATE UNIQUE INDEX "index_site_id_name_revision" ON "indexes" ("site_id", "name", "revision");
-- Create "users" table
CREATE TABLE "users" ("id" uuid NOT NULL, "create_time" timestamptz NOT NULL, "update_time" timestamptz NOT NULL, "email" character varying NOT NULL, "given_name" character varying NULL, "surname" character varying NULL, "is_admin" boolean NOT NULL DEFAULT false, "password" bytea NOT NULL, PRIMARY KEY ("id"));
-- Create index "user_email" to table: "users"
CREATE INDEX "user_email" ON "users" ("email");
-- Create index "users_email_key" to table: "users"
CREATE UNIQUE INDEX "users_email_key" ON "users" ("email");
-- Create "user_staffs" table
CREATE TABLE "user_staffs" ("id" uuid NOT NULL, "create_time" timestamptz NOT NULL, "update_time" timestamptz NOT NULL, "site_id" uuid NOT NULL, "user_id" uuid NOT NULL, PRIMARY KEY ("id"), CONSTRAINT "user_staffs_sites_site_members" FOREIGN KEY ("site_id") REFERENCES "sites" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT "user_staffs_users_staffs" FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION);

View file

@ -0,0 +1,3 @@
h1:e8aTUvgTZiilJzovujvwuOMj6Zp6nOrtbNYfCrLhj6U=
0000000000001_migrations.sql h1:a0T3g7xDO2r2e2+Xa6RKHHCG0LOVPsHZ4RBBefLgEeM=
20240613192625_init.sql h1:R4uCBBTpnV/xKAJ6UiVsXSwHNlcJSqAa+GlsMsF1k/A=

View file

@ -0,0 +1,22 @@
-- Create "migrations" table
CREATE TABLE `migrations`
(
`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT,
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
`version` text NOT NULL,
`description` text NULL,
`type` integer NOT NULL DEFAULT (0),
`applied` integer NULL,
`total` integer NULL,
`executed_at` datetime NOT NULL,
`execution_time` integer NOT NULL,
`error` text NULL,
`error_stmt` text NULL,
`hash` text NOT NULL,
`partial_hashes` json NOT NULL,
`operator_version` text NOT NULL
);
-- Create index "migrations_version_key" to table: "migrations"
CREATE UNIQUE INDEX `migrations_version_key` ON `migrations` (`version`);

View file

@ -0,0 +1,16 @@
-- Create "indexes" table
CREATE TABLE `indexes` (`id` uuid NOT NULL, `create_time` datetime NOT NULL, `update_time` datetime NOT NULL, `name` text NOT NULL, `revision` text NOT NULL, `site_id` uuid NOT NULL, PRIMARY KEY (`id`), CONSTRAINT `indexes_sites_indices` FOREIGN KEY (`site_id`) REFERENCES `sites` (`id`) ON DELETE NO ACTION);
-- Create index "index_site_id_name_revision" to table: "indexes"
CREATE UNIQUE INDEX `index_site_id_name_revision` ON `indexes` (`site_id`, `name`, `revision`);
-- Create "sites" table
CREATE TABLE `sites` (`id` uuid NOT NULL, `create_time` datetime NOT NULL, `update_time` datetime NOT NULL, `name` text NOT NULL, PRIMARY KEY (`id`));
-- Create index "sites_name_key" to table: "sites"
CREATE UNIQUE INDEX `sites_name_key` ON `sites` (`name`);
-- Create "users" table
CREATE TABLE `users` (`id` uuid NOT NULL, `create_time` datetime NOT NULL, `update_time` datetime NOT NULL, `email` text NOT NULL, `given_name` text NULL, `surname` text NULL, `is_admin` bool NOT NULL DEFAULT (false), `password` blob NOT NULL, PRIMARY KEY (`id`));
-- Create index "users_email_key" to table: "users"
CREATE UNIQUE INDEX `users_email_key` ON `users` (`email`);
-- Create index "user_email" to table: "users"
CREATE INDEX `user_email` ON `users` (`email`);
-- Create "user_staffs" table
CREATE TABLE `user_staffs` (`id` uuid NOT NULL, `create_time` datetime NOT NULL, `update_time` datetime NOT NULL, `site_id` uuid NOT NULL, `user_id` uuid NOT NULL, PRIMARY KEY (`id`), CONSTRAINT `user_staffs_sites_site_members` FOREIGN KEY (`site_id`) REFERENCES `sites` (`id`) ON DELETE NO ACTION, CONSTRAINT `user_staffs_users_staffs` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE NO ACTION);

View file

@ -0,0 +1,3 @@
h1:3BIgTNHGncd1iITEfa+rpbqA95p4H7nztnPkei2xG5s=
0000000000001_migrations.sql h1:j9JgrK+hLY1sFjpqRBSBt9h0/fLCzeEMBiqHhi6Pm4M=
20240613192622_init.sql h1:45ohikB4+ZqgsDTUkHRaShghInD1dea7TRNKjawqm5I=

1
testdata/password vendored Normal file
View file

@ -0,0 +1 @@
asdfasdfasdf

8
tools.go Normal file
View file

@ -0,0 +1,8 @@
//go:build tools
package main
import (
_ "github.com/99designs/gqlgen/api"
_ "github.com/a-h/templ"
)