From 9ea9a8f658c3eab7451ca4e17cea126ee4ad4a28 Mon Sep 17 00:00:00 2001 From: Peter Kurfer Date: Wed, 19 Jun 2024 21:19:37 +0200 Subject: [PATCH] feat: continue basic setup - setup ent scheme - add command to create users - document API - add helpers to create migrations - add command to run migrations - add basic compose file --- .devcontainer/.env | 4 + .devcontainer/Dockerfile | 13 + .devcontainer/compose.yml | 36 ++ .devcontainer/devcontainer.json | 23 + .editorconfig | 27 ++ .gitignore | 5 +- .vscode/extensions.json | 11 + .vscode/settings.json | 3 + assets/api/ent.graphql | 432 ++++++++++++++++++ assets/api/searcherside_v1.yaml | 164 +++++++ assets/api/types.graphql | 1 + compose.yml | 9 + config.yaml | 9 +- core/cq/user.go | 10 + core/domain/user.go | 12 + core/ports/db.go | 34 ++ core/ports/pw_hash.go | 6 + core/ports/repositories.go | 26 ++ core/services/argon2id_hasher.go | 143 ++++++ core/services/argon2id_hasher_test.go | 86 ++++ core/services/bleve_indexer.go | 2 +- core/services/password_hash.go | 81 ++++ core/services/password_hash_test.go | 34 ++ core/services/tar_zst_index_archiver.go | 2 +- generate.go | 5 + go.mod | 95 +++- go.sum | 315 ++++++++++++- gqlgen.yml | 36 ++ handlers/api/v1/index_handler.go | 18 +- handlers/api/v1/routes.go | 8 + handlers/api/v1/search_handler.go | 10 +- handlers/cli/migrate.go | 46 ++ handlers/cli/server.go | 42 +- handlers/cli/token.go | 14 +- handlers/cli/user_create.go | 49 ++ handlers/cli/users.go | 23 + handlers/graphql/ent.resolvers.go | 38 ++ handlers/graphql/resolver.go | 20 + .../{ => middlewares}/logging_middleware.go | 4 +- {internal => infrastructure}/archive/tar.go | 0 {internal => infrastructure}/archive/zip.go | 0 infrastructure/config/db.go | 29 ++ infrastructure/config/logging.go | 17 + infrastructure/config/yaml.go | 10 +- infrastructure/db/entc.go | 44 ++ infrastructure/db/migrate/atlas_migrator.go | 80 ++++ .../db/migrate/atlas_migrator_test.go | 71 +++ infrastructure/db/migrate/db_revision_rw.go | 144 ++++++ infrastructure/db/migrate/fs.go | 79 ++++ infrastructure/db/schema/index.go | 59 +++ infrastructure/db/schema/migration.go | 62 +++ infrastructure/db/schema/site.go | 46 ++ infrastructure/db/schema/user.go | 69 +++ infrastructure/db/schema/user_staff.go | 52 +++ infrastructure/db/schema/uuidgql/uuidgql.go | 24 + .../logging/context.go | 0 {internal => infrastructure}/logging/error.go | 0 .../repository/EntUserRepository.go | 65 +++ internal/app.go | 18 +- internal/{flags => cli}/claims.go | 2 +- internal/{flags => cli}/hex_string.go | 2 +- internal/cli/password_mapper.go | 43 ++ internal/db/dbtest/close.go | 15 + internal/db/dbtest/for_dialect.go | 19 + internal/db/dbtest/postgres.go | 39 ++ internal/db/dbtest/sqlite.go | 5 + internal/db/init.go | 13 + internal/db/pgx_driver_wrapper.go | 15 + internal/db/sqlite_driver_wrapper.go | 27 ++ internal/migrations/main.go | 77 ++++ internal/shred/shred.go | 7 + main.go | 1 + scripts/migrations/fs.go | 6 + .../postgres/0000000000001_migrations.sql | 20 + .../postgres/20240613192625_init.sql | 16 + scripts/migrations/postgres/atlas.sum | 3 + .../sqlite3/0000000000001_migrations.sql | 22 + .../sqlite3/20240613192622_init.sql | 16 + scripts/migrations/sqlite3/atlas.sum | 3 + testdata/password | 1 + tools.go | 8 + 81 files changed, 3051 insertions(+), 74 deletions(-) create mode 100644 .devcontainer/.env create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/compose.yml create mode 100644 .devcontainer/devcontainer.json create mode 100644 .editorconfig create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 assets/api/ent.graphql create mode 100644 assets/api/searcherside_v1.yaml create mode 100644 assets/api/types.graphql create mode 100644 compose.yml create mode 100644 core/cq/user.go create mode 100644 core/domain/user.go create mode 100644 core/ports/db.go create mode 100644 core/ports/pw_hash.go create mode 100644 core/ports/repositories.go create mode 100644 core/services/argon2id_hasher.go create mode 100644 core/services/argon2id_hasher_test.go create mode 100644 core/services/password_hash.go create mode 100644 core/services/password_hash_test.go create mode 100644 generate.go create mode 100644 gqlgen.yml create mode 100644 handlers/cli/migrate.go create mode 100644 handlers/cli/user_create.go create mode 100644 handlers/cli/users.go create mode 100644 handlers/graphql/ent.resolvers.go create mode 100644 handlers/graphql/resolver.go rename infrastructure/api/{ => middlewares}/logging_middleware.go (92%) rename {internal => infrastructure}/archive/tar.go (100%) rename {internal => infrastructure}/archive/zip.go (100%) create mode 100644 infrastructure/config/db.go create mode 100644 infrastructure/config/logging.go create mode 100644 infrastructure/db/entc.go create mode 100644 infrastructure/db/migrate/atlas_migrator.go create mode 100644 infrastructure/db/migrate/atlas_migrator_test.go create mode 100644 infrastructure/db/migrate/db_revision_rw.go create mode 100644 infrastructure/db/migrate/fs.go create mode 100644 infrastructure/db/schema/index.go create mode 100644 infrastructure/db/schema/migration.go create mode 100644 infrastructure/db/schema/site.go create mode 100644 infrastructure/db/schema/user.go create mode 100644 infrastructure/db/schema/user_staff.go create mode 100644 infrastructure/db/schema/uuidgql/uuidgql.go rename {internal => infrastructure}/logging/context.go (100%) rename {internal => infrastructure}/logging/error.go (100%) create mode 100644 infrastructure/repository/EntUserRepository.go rename internal/{flags => cli}/claims.go (97%) rename internal/{flags => cli}/hex_string.go (96%) create mode 100644 internal/cli/password_mapper.go create mode 100644 internal/db/dbtest/close.go create mode 100644 internal/db/dbtest/for_dialect.go create mode 100644 internal/db/dbtest/postgres.go create mode 100644 internal/db/dbtest/sqlite.go create mode 100644 internal/db/init.go create mode 100644 internal/db/pgx_driver_wrapper.go create mode 100644 internal/db/sqlite_driver_wrapper.go create mode 100644 internal/migrations/main.go create mode 100644 internal/shred/shred.go create mode 100644 scripts/migrations/fs.go create mode 100644 scripts/migrations/postgres/0000000000001_migrations.sql create mode 100644 scripts/migrations/postgres/20240613192625_init.sql create mode 100644 scripts/migrations/postgres/atlas.sum create mode 100644 scripts/migrations/sqlite3/0000000000001_migrations.sql create mode 100644 scripts/migrations/sqlite3/20240613192622_init.sql create mode 100644 scripts/migrations/sqlite3/atlas.sum create mode 100644 testdata/password create mode 100644 tools.go diff --git a/.devcontainer/.env b/.devcontainer/.env new file mode 100644 index 0000000..04b7ac2 --- /dev/null +++ b/.devcontainer/.env @@ -0,0 +1,4 @@ +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_DB=postgres +POSTGRES_HOSTNAME=localhost \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000..7214876 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -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 + +# [Optional] Uncomment the next lines to use go get to install anything else you need +# USER vscode +# RUN go get -x +# 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 " 2>&1 \ No newline at end of file diff --git a/.devcontainer/compose.yml b/.devcontainer/compose.yml new file mode 100644 index 0000000..d74e1cd --- /dev/null +++ b/.devcontainer/compose.yml @@ -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.) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..000ae87 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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" +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2906418 --- /dev/null +++ b/.editorconfig @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore index fd1f319..779f147 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,7 @@ data/ # IDE configs .idea/ -.vscode/ \ No newline at end of file + +# Generated files +generated/ +internal/ent/ \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..de87263 --- /dev/null +++ b/.vscode/extensions.json @@ -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" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..582a161 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "redhat.telemetry.enabled": false +} \ No newline at end of file diff --git a/assets/api/ent.graphql b/assets/api/ent.graphql new file mode 100644 index 0000000..880cd9b --- /dev/null +++ b/assets/api/ent.graphql @@ -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!] +} diff --git a/assets/api/searcherside_v1.yaml b/assets/api/searcherside_v1.yaml new file mode 100644 index 0000000..3b6dcb4 --- /dev/null +++ b/assets/api/searcherside_v1.yaml @@ -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 / + 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 / + 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 diff --git a/assets/api/types.graphql b/assets/api/types.graphql new file mode 100644 index 0000000..7c75f91 --- /dev/null +++ b/assets/api/types.graphql @@ -0,0 +1 @@ +scalar UUID \ No newline at end of file diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..ed06d5a --- /dev/null +++ b/compose.yml @@ -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" diff --git a/config.yaml b/config.yaml index 036110d..401fd71 100644 --- a/config.yaml +++ b/config.yaml @@ -1,3 +1,6 @@ -server: - http: - readHeaderTimeout: 15s \ No newline at end of file +http: + readHeaderTimeout: 15s + +db: + driver: postgres + url: 'postgresql://searcherside:1n1t-r00t!@localhost:5432/searcherside?sslmode=disable' diff --git a/core/cq/user.go b/core/cq/user.go new file mode 100644 index 0000000..da42bfa --- /dev/null +++ b/core/cq/user.go @@ -0,0 +1,10 @@ +package cq + +type CreateUserRequest struct { + Email string + Password []byte + Admin bool +} + +type CreateUserResponse struct { +} diff --git a/core/domain/user.go b/core/domain/user.go new file mode 100644 index 0000000..17dfbd1 --- /dev/null +++ b/core/domain/user.go @@ -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 +} diff --git a/core/ports/db.go b/core/ports/db.go new file mode 100644 index 0000000..3b84ae7 --- /dev/null +++ b/core/ports/db.go @@ -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 +} diff --git a/core/ports/pw_hash.go b/core/ports/pw_hash.go new file mode 100644 index 0000000..bc6fd70 --- /dev/null +++ b/core/ports/pw_hash.go @@ -0,0 +1,6 @@ +package ports + +type PasswordHashAlgorithm interface { + Hash(password []byte) ([]byte, error) + Validate(password []byte, hash []byte) error +} diff --git a/core/ports/repositories.go b/core/ports/repositories.go new file mode 100644 index 0000000..a000414 --- /dev/null +++ b/core/ports/repositories.go @@ -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 +} diff --git a/core/services/argon2id_hasher.go b/core/services/argon2id_hasher.go new file mode 100644 index 0000000..16f7a75 --- /dev/null +++ b/core/services/argon2id_hasher.go @@ -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 + } +} diff --git a/core/services/argon2id_hasher_test.go b/core/services/argon2id_hasher_test.go new file mode 100644 index 0000000..dec3afb --- /dev/null +++ b/core/services/argon2id_hasher_test.go @@ -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)) + }) + } +} diff --git a/core/services/bleve_indexer.go b/core/services/bleve_indexer.go index 522bb0f..f705c4e 100644 --- a/core/services/bleve_indexer.go +++ b/core/services/bleve_indexer.go @@ -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) diff --git a/core/services/password_hash.go b/core/services/password_hash.go new file mode 100644 index 0000000..40f6449 --- /dev/null +++ b/core/services/password_hash.go @@ -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 +} diff --git a/core/services/password_hash_test.go b/core/services/password_hash_test.go new file mode 100644 index 0000000..8dde03a --- /dev/null +++ b/core/services/password_hash_test.go @@ -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) + } + }) + } +} diff --git a/core/services/tar_zst_index_archiver.go b/core/services/tar_zst_index_archiver.go index 135db6e..d41117b 100644 --- a/core/services/tar_zst_index_archiver.go +++ b/core/services/tar_zst_index_archiver.go @@ -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) diff --git a/generate.go b/generate.go new file mode 100644 index 0000000..d559d9f --- /dev/null +++ b/generate.go @@ -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 diff --git a/go.mod b/go.mod index c3f2ab6..51d60e6 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index f36c49a..cf1b199 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/gqlgen.yml b/gqlgen.yml new file mode 100644 index 0000000..5e19421 --- /dev/null +++ b/gqlgen.yml @@ -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 \ No newline at end of file diff --git a/handlers/api/v1/index_handler.go b/handlers/api/v1/index_handler.go index 295391b..755b896 100644 --- a/handlers/api/v1/index_handler.go +++ b/handlers/api/v1/index_handler.go @@ -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)) } }() diff --git a/handlers/api/v1/routes.go b/handlers/api/v1/routes.go index a731c60..0a8a460 100644 --- a/handlers/api/v1/routes.go +++ b/handlers/api/v1/routes.go @@ -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) diff --git a/handlers/api/v1/search_handler.go b/handlers/api/v1/search_handler.go index 1877240..21d8400 100644 --- a/handlers/api/v1/search_handler.go +++ b/handlers/api/v1/search_handler.go @@ -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 } diff --git a/handlers/cli/migrate.go b/handlers/cli/migrate.go new file mode 100644 index 0000000..617ed04 --- /dev/null +++ b/handlers/cli/migrate.go @@ -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 +} diff --git a/handlers/cli/server.go b/handlers/cli/server.go index 432be8f..9f7fe6d 100644 --- a/handlers/cli/server.go +++ b/handlers/cli/server.go @@ -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) diff --git a/handlers/cli/token.go b/handlers/cli/token.go index 243fe95..b74e24d 100644 --- a/handlers/cli/token.go +++ b/handlers/cli/token.go @@ -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"` - 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"` + 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 []cli.TokenClaim `name:"claims" help:"JWT claims"` } `embed:"" prefix:"token."` } diff --git a/handlers/cli/user_create.go b/handlers/cli/user_create.go new file mode 100644 index 0000000..dc8e3ce --- /dev/null +++ b/handlers/cli/user_create.go @@ -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 +} diff --git a/handlers/cli/users.go b/handlers/cli/users.go new file mode 100644 index 0000000..e2e450f --- /dev/null +++ b/handlers/cli/users.go @@ -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 +} diff --git a/handlers/graphql/ent.resolvers.go b/handlers/graphql/ent.resolvers.go new file mode 100644 index 0000000..ba0b48d --- /dev/null +++ b/handlers/graphql/ent.resolvers.go @@ -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 } diff --git a/handlers/graphql/resolver.go b/handlers/graphql/resolver.go new file mode 100644 index 0000000..08c60f9 --- /dev/null +++ b/handlers/graphql/resolver.go @@ -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 +} diff --git a/infrastructure/api/logging_middleware.go b/infrastructure/api/middlewares/logging_middleware.go similarity index 92% rename from infrastructure/api/logging_middleware.go rename to infrastructure/api/middlewares/logging_middleware.go index 713f89c..2c08cde 100644 --- a/infrastructure/api/logging_middleware.go +++ b/infrastructure/api/middlewares/logging_middleware.go @@ -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 { diff --git a/internal/archive/tar.go b/infrastructure/archive/tar.go similarity index 100% rename from internal/archive/tar.go rename to infrastructure/archive/tar.go diff --git a/internal/archive/zip.go b/infrastructure/archive/zip.go similarity index 100% rename from internal/archive/zip.go rename to infrastructure/archive/zip.go diff --git a/infrastructure/config/db.go b/infrastructure/config/db.go new file mode 100644 index 0000000..5951452 --- /dev/null +++ b/infrastructure/config/db.go @@ -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 +} diff --git a/infrastructure/config/logging.go b/infrastructure/config/logging.go new file mode 100644 index 0000000..9e09bf1 --- /dev/null +++ b/infrastructure/config/logging.go @@ -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, + } +} diff --git a/infrastructure/config/yaml.go b/infrastructure/config/yaml.go index cd3a97b..32e08a1 100644 --- a/infrastructure/config/yaml.go +++ b/infrastructure/config/yaml.go @@ -56,14 +56,8 @@ 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 { - return val, nil - } - } + if val := lookup(config, y.normalize(strings.Split(flag.Name, y.separator()))); val != nil { + return val, nil } return nil, nil diff --git a/infrastructure/db/entc.go b/infrastructure/db/entc.go new file mode 100644 index 0000000..98f961b --- /dev/null +++ b/infrastructure/db/entc.go @@ -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) + } +} diff --git a/infrastructure/db/migrate/atlas_migrator.go b/infrastructure/db/migrate/atlas_migrator.go new file mode 100644 index 0000000..d0ed236 --- /dev/null +++ b/infrastructure/db/migrate/atlas_migrator.go @@ -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) + } +} diff --git a/infrastructure/db/migrate/atlas_migrator_test.go b/infrastructure/db/migrate/atlas_migrator_test.go new file mode 100644 index 0000000..2a1b34b --- /dev/null +++ b/infrastructure/db/migrate/atlas_migrator_test.go @@ -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) + } +} diff --git a/infrastructure/db/migrate/db_revision_rw.go b/infrastructure/db/migrate/db_revision_rw.go new file mode 100644 index 0000000..58a6b65 --- /dev/null +++ b/infrastructure/db/migrate/db_revision_rw.go @@ -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 +} diff --git a/infrastructure/db/migrate/fs.go b/infrastructure/db/migrate/fs.go new file mode 100644 index 0000000..76db2a7 --- /dev/null +++ b/infrastructure/db/migrate/fs.go @@ -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 +} diff --git a/infrastructure/db/schema/index.go b/infrastructure/db/schema/index.go new file mode 100644 index 0000000..bc1f137 --- /dev/null +++ b/infrastructure/db/schema/index.go @@ -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{}, + } +} diff --git a/infrastructure/db/schema/migration.go b/infrastructure/db/schema/migration.go new file mode 100644 index 0000000..f87ecf9 --- /dev/null +++ b/infrastructure/db/schema/migration.go @@ -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), + } +} diff --git a/infrastructure/db/schema/site.go b/infrastructure/db/schema/site.go new file mode 100644 index 0000000..62b0463 --- /dev/null +++ b/infrastructure/db/schema/site.go @@ -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(), + } +} diff --git a/infrastructure/db/schema/user.go b/infrastructure/db/schema/user.go new file mode 100644 index 0000000..d09c1ed --- /dev/null +++ b/infrastructure/db/schema/user.go @@ -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(), + } +} diff --git a/infrastructure/db/schema/user_staff.go b/infrastructure/db/schema/user_staff.go new file mode 100644 index 0000000..7369a49 --- /dev/null +++ b/infrastructure/db/schema/user_staff.go @@ -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{}, + } +} diff --git a/infrastructure/db/schema/uuidgql/uuidgql.go b/infrastructure/db/schema/uuidgql/uuidgql.go new file mode 100644 index 0000000..4b14a4e --- /dev/null +++ b/infrastructure/db/schema/uuidgql/uuidgql.go @@ -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) +} diff --git a/internal/logging/context.go b/infrastructure/logging/context.go similarity index 100% rename from internal/logging/context.go rename to infrastructure/logging/context.go diff --git a/internal/logging/error.go b/infrastructure/logging/error.go similarity index 100% rename from internal/logging/error.go rename to infrastructure/logging/error.go diff --git a/infrastructure/repository/EntUserRepository.go b/infrastructure/repository/EntUserRepository.go new file mode 100644 index 0000000..a61520f --- /dev/null +++ b/infrastructure/repository/EntUserRepository.go @@ -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") +} diff --git a/internal/app.go b/internal/app.go index db88822..501b49e 100644 --- a/internal/app.go +++ b/internal/app.go @@ -12,15 +12,16 @@ 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"` - Token clih.TokenHandler `cmd:"" name:"token" help:"Generate a token"` + 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"` } func (a *App) Execute() error { @@ -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) diff --git a/internal/flags/claims.go b/internal/cli/claims.go similarity index 97% rename from internal/flags/claims.go rename to internal/cli/claims.go index b7ebc60..aa0bfaa 100644 --- a/internal/flags/claims.go +++ b/internal/cli/claims.go @@ -1,4 +1,4 @@ -package flags +package cli import ( "fmt" diff --git a/internal/flags/hex_string.go b/internal/cli/hex_string.go similarity index 96% rename from internal/flags/hex_string.go rename to internal/cli/hex_string.go index 0b1ccc1..9d70bc4 100644 --- a/internal/flags/hex_string.go +++ b/internal/cli/hex_string.go @@ -1,4 +1,4 @@ -package flags +package cli import ( "encoding/hex" diff --git a/internal/cli/password_mapper.go b/internal/cli/password_mapper.go new file mode 100644 index 0000000..f469243 --- /dev/null +++ b/internal/cli/password_mapper.go @@ -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 +} diff --git a/internal/db/dbtest/close.go b/internal/db/dbtest/close.go new file mode 100644 index 0000000..70a541c --- /dev/null +++ b/internal/db/dbtest/close.go @@ -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) } diff --git a/internal/db/dbtest/for_dialect.go b/internal/db/dbtest/for_dialect.go new file mode 100644 index 0000000..7945dcd --- /dev/null +++ b/internal/db/dbtest/for_dialect.go @@ -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) + } +} diff --git a/internal/db/dbtest/postgres.go b/internal/db/dbtest/postgres.go new file mode 100644 index 0000000..517d461 --- /dev/null +++ b/internal/db/dbtest/postgres.go @@ -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 + } +} diff --git a/internal/db/dbtest/sqlite.go b/internal/db/dbtest/sqlite.go new file mode 100644 index 0000000..c7f6fd6 --- /dev/null +++ b/internal/db/dbtest/sqlite.go @@ -0,0 +1,5 @@ +package dbtest + +func TestSQLiteDB() (connString string) { + return "sqlite://dev?mode=memory" +} diff --git a/internal/db/init.go b/internal/db/init.go new file mode 100644 index 0000000..6c7758f --- /dev/null +++ b/internal/db/init.go @@ -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)}) +} diff --git a/internal/db/pgx_driver_wrapper.go b/internal/db/pgx_driver_wrapper.go new file mode 100644 index 0000000..eb25c07 --- /dev/null +++ b/internal/db/pgx_driver_wrapper.go @@ -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) +} diff --git a/internal/db/sqlite_driver_wrapper.go b/internal/db/sqlite_driver_wrapper.go new file mode 100644 index 0000000..8abfebf --- /dev/null +++ b/internal/db/sqlite_driver_wrapper.go @@ -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 +} diff --git a/internal/migrations/main.go b/internal/migrations/main.go new file mode 100644 index 0000000..892f39a --- /dev/null +++ b/internal/migrations/main.go @@ -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 '") + } + + 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...) +} diff --git a/internal/shred/shred.go b/internal/shred/shred.go new file mode 100644 index 0000000..fac6cc1 --- /dev/null +++ b/internal/shred/shred.go @@ -0,0 +1,7 @@ +package shred + +func Bytes(b []byte) { + for i := range b { + b[i] = 0 + } +} diff --git a/main.go b/main.go index 2cce324..ad75dcb 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( "os" "code.icb4dc0.de/prskr/searcherside/internal" + _ "code.icb4dc0.de/prskr/searcherside/internal/db" ) func main() { diff --git a/scripts/migrations/fs.go b/scripts/migrations/fs.go new file mode 100644 index 0000000..0446c5a --- /dev/null +++ b/scripts/migrations/fs.go @@ -0,0 +1,6 @@ +package migrations + +import "embed" + +//go:embed postgres/* sqlite3/* +var Fs embed.FS diff --git a/scripts/migrations/postgres/0000000000001_migrations.sql b/scripts/migrations/postgres/0000000000001_migrations.sql new file mode 100644 index 0000000..9c98ff5 --- /dev/null +++ b/scripts/migrations/postgres/0000000000001_migrations.sql @@ -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") +); \ No newline at end of file diff --git a/scripts/migrations/postgres/20240613192625_init.sql b/scripts/migrations/postgres/20240613192625_init.sql new file mode 100644 index 0000000..7067365 --- /dev/null +++ b/scripts/migrations/postgres/20240613192625_init.sql @@ -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); diff --git a/scripts/migrations/postgres/atlas.sum b/scripts/migrations/postgres/atlas.sum new file mode 100644 index 0000000..08bf58c --- /dev/null +++ b/scripts/migrations/postgres/atlas.sum @@ -0,0 +1,3 @@ +h1:e8aTUvgTZiilJzovujvwuOMj6Zp6nOrtbNYfCrLhj6U= +0000000000001_migrations.sql h1:a0T3g7xDO2r2e2+Xa6RKHHCG0LOVPsHZ4RBBefLgEeM= +20240613192625_init.sql h1:R4uCBBTpnV/xKAJ6UiVsXSwHNlcJSqAa+GlsMsF1k/A= diff --git a/scripts/migrations/sqlite3/0000000000001_migrations.sql b/scripts/migrations/sqlite3/0000000000001_migrations.sql new file mode 100644 index 0000000..a559d05 --- /dev/null +++ b/scripts/migrations/sqlite3/0000000000001_migrations.sql @@ -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`); \ No newline at end of file diff --git a/scripts/migrations/sqlite3/20240613192622_init.sql b/scripts/migrations/sqlite3/20240613192622_init.sql new file mode 100644 index 0000000..6ae1f0a --- /dev/null +++ b/scripts/migrations/sqlite3/20240613192622_init.sql @@ -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); diff --git a/scripts/migrations/sqlite3/atlas.sum b/scripts/migrations/sqlite3/atlas.sum new file mode 100644 index 0000000..d81343c --- /dev/null +++ b/scripts/migrations/sqlite3/atlas.sum @@ -0,0 +1,3 @@ +h1:3BIgTNHGncd1iITEfa+rpbqA95p4H7nztnPkei2xG5s= +0000000000001_migrations.sql h1:j9JgrK+hLY1sFjpqRBSBt9h0/fLCzeEMBiqHhi6Pm4M= +20240613192622_init.sql h1:45ohikB4+ZqgsDTUkHRaShghInD1dea7TRNKjawqm5I= diff --git a/testdata/password b/testdata/password new file mode 100644 index 0000000..9132c66 --- /dev/null +++ b/testdata/password @@ -0,0 +1 @@ +asdfasdfasdf \ No newline at end of file diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..ef71440 --- /dev/null +++ b/tools.go @@ -0,0 +1,8 @@ +//go:build tools + +package main + +import ( + _ "github.com/99designs/gqlgen/api" + _ "github.com/a-h/templ" +)