From 29944915cdf12921041d1e1c45e60f1cd0160f99 Mon Sep 17 00:00:00 2001 From: AzenKain Date: Tue, 5 May 2026 16:57:44 +0700 Subject: [PATCH] UPDATE: new db --- db/migrations/000007_entities.up.sql | 5 +- db/migrations/000008_wiki.up.sql | 4 + db/query/entities.sql | 5 + db/query/wiki.sql | 10 +- db/schema.sql | 7 +- docs/docs.go | 160 ++++++++++++++++++++- docs/swagger.json | 160 ++++++++++++++++++++- docs/swagger.yaml | 106 +++++++++++++- internal/controllers/entityController.go | 55 +++++++ internal/controllers/wikiController.go | 55 +++++++ internal/dtos/request/snapshot.go | 17 +-- internal/dtos/response/wiki.go | 1 + internal/gen/sqlc/entities.sql.go | 25 ++++ internal/gen/sqlc/models.go | 1 + internal/gen/sqlc/wiki.sql.go | 55 +++++-- internal/models/wiki.go | 2 + internal/repositories/entityRepository.go | 166 +++++++++++++--------- internal/repositories/wikiRepository.go | 41 ++++++ internal/routes/entityRoute.go | 3 + internal/routes/wikiRoute.go | 3 + internal/services/entityService.go | 27 ++++ internal/services/submissionService.go | 55 +++++-- internal/services/wikiService.go | 27 ++++ pkg/constants/regex.go | 3 +- pkg/validator/validator.go | 10 ++ 25 files changed, 893 insertions(+), 110 deletions(-) diff --git a/db/migrations/000007_entities.up.sql b/db/migrations/000007_entities.up.sql index a47455b..d033961 100644 --- a/db/migrations/000007_entities.up.sql +++ b/db/migrations/000007_entities.up.sql @@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS entities ( id UUID PRIMARY KEY DEFAULT uuidv7(), project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, name TEXT NOT NULL, - slug TEXT, + slug TEXT UNIQUE, description TEXT, status SMALLINT, time_start INT, @@ -12,6 +12,9 @@ CREATE TABLE IF NOT EXISTS entities ( updated_at TIMESTAMPTZ DEFAULT now() ); +CREATE UNIQUE INDEX idx_entities_slug_not_deleted +ON entities(slug) +WHERE is_deleted = false; CREATE INDEX idx_entities_name_search ON entities USING GIN (name gin_trgm_ops); diff --git a/db/migrations/000008_wiki.up.sql b/db/migrations/000008_wiki.up.sql index db1e7b6..d07f0ea 100644 --- a/db/migrations/000008_wiki.up.sql +++ b/db/migrations/000008_wiki.up.sql @@ -4,12 +4,16 @@ CREATE TABLE IF NOT EXISTS wikis ( id UUID PRIMARY KEY DEFAULT uuidv7(), project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, title TEXT, + slug TEXT, content TEXT, is_deleted BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); +CREATE UNIQUE INDEX idx_wikis_slug_not_deleted +ON wikis(slug) +WHERE is_deleted = false; CREATE TABLE IF NOT EXISTS entity_wikis ( entity_id UUID REFERENCES entities(id) ON DELETE CASCADE, diff --git a/db/query/entities.sql b/db/query/entities.sql index a818462..346d4a8 100644 --- a/db/query/entities.sql +++ b/db/query/entities.sql @@ -60,3 +60,8 @@ WHERE project_id = $1 AND is_deleted = false; UPDATE entities SET is_deleted = true WHERE id = ANY($1::uuid[]); + +-- name: GetEntityBySlug :one +SELECT * +FROM entities +WHERE slug = $1 AND is_deleted = false; \ No newline at end of file diff --git a/db/query/wiki.sql b/db/query/wiki.sql index 990c302..0b1e402 100644 --- a/db/query/wiki.sql +++ b/db/query/wiki.sql @@ -1,8 +1,8 @@ -- name: CreateWiki :one INSERT INTO wikis ( - id, title, content, project_id + id, title, slug, content, project_id ) VALUES ( - COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3 + COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3, $4 ) RETURNING *; @@ -15,6 +15,7 @@ WHERE id = $1 AND is_deleted = false; UPDATE wikis SET title = COALESCE(sqlc.narg('title'), title), + slug = COALESCE(sqlc.narg('slug'), slug), content = COALESCE(sqlc.narg('content'), content), project_id = COALESCE(sqlc.narg('project_id'), project_id) WHERE id = sqlc.arg('id') AND is_deleted = false @@ -84,3 +85,8 @@ WHERE wiki_id = $1; -- name: DeleteEntityWiki :exec DELETE FROM entity_wikis WHERE entity_id = $1 AND wiki_id = $2; + +-- name: GetWikiBySlug :one +SELECT * +FROM wikis +WHERE slug = $1 AND is_deleted = false; diff --git a/db/schema.sql b/db/schema.sql index 122fea0..fd7bab0 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -87,7 +87,7 @@ CREATE TABLE IF NOT EXISTS entities ( id UUID PRIMARY KEY DEFAULT uuidv7(), project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, name TEXT NOT NULL, - slug TEXT, + slug TEXT UNIQUE, description TEXT, status SMALLINT, time_start INT, @@ -101,6 +101,7 @@ CREATE TABLE IF NOT EXISTS wikis ( id UUID PRIMARY KEY DEFAULT uuidv7(), project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, title TEXT, + slug TEXT UNIQUE, content TEXT, is_deleted BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ DEFAULT now(), @@ -114,8 +115,6 @@ CREATE TABLE IF NOT EXISTS entity_wikis ( PRIMARY KEY (entity_id, wiki_id) ); -CREATE INDEX idx_entity_wikis_project_id ON entity_wikis(project_id); - CREATE TABLE IF NOT EXISTS geometries ( id UUID PRIMARY KEY DEFAULT uuidv7(), geo_type SMALLINT NOT NULL DEFAULT 1, @@ -137,8 +136,6 @@ CREATE TABLE IF NOT EXISTS entity_geometries ( PRIMARY KEY (entity_id, geometry_id) ); -CREATE INDEX idx_entity_geometries_project_id ON entity_geometries(project_id); - CREATE TABLE IF NOT EXISTS commits ( id UUID PRIMARY KEY DEFAULT uuidv7(), project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, diff --git a/docs/docs.go b/docs/docs.go index 5cef8d7..faff404 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -403,7 +403,7 @@ const docTemplate = `{ "post": { "security": [ { - "ApiKeyAuth": [] + "BearerAuth": [] } ], "description": "Ask a history question based on project context or global knowledge using RAG", @@ -516,6 +516,82 @@ const docTemplate = `{ } } }, + "/entities/slug/exists": { + "get": { + "description": "Check if a given slug already exists for entities", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Entities" + ], + "summary": "Check entity slug existence", + "parameters": [ + { + "type": "string", + "description": "Slug to check", + "name": "slug", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + } + } + } + }, + "/entities/slug/{slug}": { + "get": { + "description": "Get detailed information about a specific entity by its unique slug", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Entities" + ], + "summary": "Get entity by slug", + "parameters": [ + { + "type": "string", + "description": "Entity Slug", + "name": "slug", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + } + } + } + }, "/entities/{id}": { "get": { "description": "Get detailed information about a specific entity", @@ -3519,6 +3595,82 @@ const docTemplate = `{ } } }, + "/wikis/slug/exists": { + "get": { + "description": "Check if a given slug already exists for wikis", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Wikis" + ], + "summary": "Check wiki slug existence", + "parameters": [ + { + "type": "string", + "description": "Slug to check", + "name": "slug", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + } + } + } + }, + "/wikis/slug/{slug}": { + "get": { + "description": "Get detailed information about a specific wiki by its unique slug", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Wikis" + ], + "summary": "Get wiki by slug", + "parameters": [ + { + "type": "string", + "description": "Wiki Slug", + "name": "slug", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + } + } + } + }, "/wikis/{id}": { "get": { "description": "Get detailed information about a specific wiki", @@ -3847,7 +3999,8 @@ const docTemplate = `{ "history-api_internal_dtos_request.EntitySnapshot": { "type": "object", "required": [ - "id" + "id", + "name" ], "properties": { "base_hash": { @@ -4360,6 +4513,9 @@ const docTemplate = `{ "reference" ] }, + "slug": { + "type": "string" + }, "source": { "type": "string", "enum": [ diff --git a/docs/swagger.json b/docs/swagger.json index b02b389..d673507 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -396,7 +396,7 @@ "post": { "security": [ { - "ApiKeyAuth": [] + "BearerAuth": [] } ], "description": "Ask a history question based on project context or global knowledge using RAG", @@ -509,6 +509,82 @@ } } }, + "/entities/slug/exists": { + "get": { + "description": "Check if a given slug already exists for entities", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Entities" + ], + "summary": "Check entity slug existence", + "parameters": [ + { + "type": "string", + "description": "Slug to check", + "name": "slug", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + } + } + } + }, + "/entities/slug/{slug}": { + "get": { + "description": "Get detailed information about a specific entity by its unique slug", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Entities" + ], + "summary": "Get entity by slug", + "parameters": [ + { + "type": "string", + "description": "Entity Slug", + "name": "slug", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + } + } + } + }, "/entities/{id}": { "get": { "description": "Get detailed information about a specific entity", @@ -3512,6 +3588,82 @@ } } }, + "/wikis/slug/exists": { + "get": { + "description": "Check if a given slug already exists for wikis", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Wikis" + ], + "summary": "Check wiki slug existence", + "parameters": [ + { + "type": "string", + "description": "Slug to check", + "name": "slug", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + } + } + } + }, + "/wikis/slug/{slug}": { + "get": { + "description": "Get detailed information about a specific wiki by its unique slug", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Wikis" + ], + "summary": "Get wiki by slug", + "parameters": [ + { + "type": "string", + "description": "Wiki Slug", + "name": "slug", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/history-api_internal_dtos_response.CommonResponse" + } + } + } + } + }, "/wikis/{id}": { "get": { "description": "Get detailed information about a specific wiki", @@ -3840,7 +3992,8 @@ "history-api_internal_dtos_request.EntitySnapshot": { "type": "object", "required": [ - "id" + "id", + "name" ], "properties": { "base_hash": { @@ -4353,6 +4506,9 @@ "reference" ] }, + "slug": { + "type": "string" + }, "source": { "type": "string", "enum": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index f31dfa8..71ec46f 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -232,6 +232,7 @@ definitions: type: number required: - id + - name type: object history-api_internal_dtos_request.EntityWikiLinkSnapshot: properties: @@ -550,6 +551,8 @@ definitions: - delete - reference type: string + slug: + type: string source: enum: - inline @@ -899,7 +902,7 @@ paths: schema: $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' security: - - ApiKeyAuth: [] + - BearerAuth: [] summary: Ask the AI chatbot tags: - Chatbot @@ -963,6 +966,57 @@ paths: summary: Get entity by ID tags: - Entities + /entities/slug/{slug}: + get: + consumes: + - application/json + description: Get detailed information about a specific entity by its unique + slug + parameters: + - description: Entity Slug + in: path + name: slug + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' + summary: Get entity by slug + tags: + - Entities + /entities/slug/exists: + get: + consumes: + - application/json + description: Check if a given slug already exists for entities + parameters: + - description: Slug to check + in: query + name: slug + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' + summary: Check entity slug existence + tags: + - Entities /geometries: get: consumes: @@ -2902,6 +2956,56 @@ paths: summary: Get wiki by ID tags: - Wikis + /wikis/slug/{slug}: + get: + consumes: + - application/json + description: Get detailed information about a specific wiki by its unique slug + parameters: + - description: Wiki Slug + in: path + name: slug + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' + summary: Get wiki by slug + tags: + - Wikis + /wikis/slug/exists: + get: + consumes: + - application/json + description: Check if a given slug already exists for wikis + parameters: + - description: Slug to check + in: query + name: slug + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' + summary: Check wiki slug existence + tags: + - Wikis securityDefinitions: BearerAuth: description: Type "Bearer " followed by a space and JWT token. diff --git a/internal/controllers/entityController.go b/internal/controllers/entityController.go index 9a14c86..a662b0a 100644 --- a/internal/controllers/entityController.go +++ b/internal/controllers/entityController.go @@ -46,6 +46,60 @@ func (h *EntityController) GetEntityById(c fiber.Ctx) error { }) } +// GetEntityBySlug handles fetching a single entity by slug. +// @Summary Get entity by slug +// @Description Get detailed information about a specific entity by its unique slug +// @Tags Entities +// @Accept json +// @Produce json +// @Param slug path string true "Entity Slug" +// @Success 200 {object} response.CommonResponse +// @Failure 404 {object} response.CommonResponse +// @Router /entities/slug/{slug} [get] +func (h *EntityController) GetEntityBySlug(c fiber.Ctx) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + slug := c.Params("slug") + res, err := h.service.GetEntityBySlug(ctx, slug) + if err != nil { + return c.Status(err.Code).JSON(response.CommonResponse{ + Status: false, + Message: err.Message, + }) + } + return c.Status(fiber.StatusOK).JSON(response.CommonResponse{ + Status: true, + Data: res, + }) +} + +// IsExistEntitySlug checks if an entity slug already exists. +// @Summary Check entity slug existence +// @Description Check if a given slug already exists for entities +// @Tags Entities +// @Accept json +// @Produce json +// @Param slug query string true "Slug to check" +// @Success 200 {object} response.CommonResponse +// @Failure 400 {object} response.CommonResponse +// @Router /entities/slug/exists [get] +func (h *EntityController) IsExistEntitySlug(c fiber.Ctx) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + slug := c.Query("slug") + exists, err := h.service.IsExistEntitySlug(ctx, slug) + if err != nil { + return c.Status(err.Code).JSON(response.CommonResponse{ + Status: false, + Message: err.Message, + }) + } + return c.Status(fiber.StatusOK).JSON(response.CommonResponse{ + Status: true, + Data: map[string]bool{"exists": exists}, + }) +} + // SearchEntities handles searching for entities. // @Summary Search entities // @Description Search entities with cursor pagination @@ -81,3 +135,4 @@ func (h *EntityController) SearchEntities(c fiber.Ctx) error { Data: res, }) } + diff --git a/internal/controllers/wikiController.go b/internal/controllers/wikiController.go index 0bf109f..ea16c1e 100644 --- a/internal/controllers/wikiController.go +++ b/internal/controllers/wikiController.go @@ -46,6 +46,60 @@ func (h *WikiController) GetWikiById(c fiber.Ctx) error { }) } +// GetWikiBySlug handles fetching a single wiki by slug. +// @Summary Get wiki by slug +// @Description Get detailed information about a specific wiki by its unique slug +// @Tags Wikis +// @Accept json +// @Produce json +// @Param slug path string true "Wiki Slug" +// @Success 200 {object} response.CommonResponse +// @Failure 404 {object} response.CommonResponse +// @Router /wikis/slug/{slug} [get] +func (h *WikiController) GetWikiBySlug(c fiber.Ctx) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + slug := c.Params("slug") + res, err := h.service.GetWikiBySlug(ctx, slug) + if err != nil { + return c.Status(err.Code).JSON(response.CommonResponse{ + Status: false, + Message: err.Message, + }) + } + return c.Status(fiber.StatusOK).JSON(response.CommonResponse{ + Status: true, + Data: res, + }) +} + +// IsExistWikiSlug checks if a wiki slug already exists. +// @Summary Check wiki slug existence +// @Description Check if a given slug already exists for wikis +// @Tags Wikis +// @Accept json +// @Produce json +// @Param slug query string true "Slug to check" +// @Success 200 {object} response.CommonResponse +// @Failure 400 {object} response.CommonResponse +// @Router /wikis/slug/exists [get] +func (h *WikiController) IsExistWikiSlug(c fiber.Ctx) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + slug := c.Query("slug") + exists, err := h.service.IsExistWikiSlug(ctx, slug) + if err != nil { + return c.Status(err.Code).JSON(response.CommonResponse{ + Status: false, + Message: err.Message, + }) + } + return c.Status(fiber.StatusOK).JSON(response.CommonResponse{ + Status: true, + Data: map[string]bool{"exists": exists}, + }) +} + // SearchWikis handles searching for wikis. // @Summary Search wikis // @Description Search wikis with cursor pagination @@ -81,3 +135,4 @@ func (h *WikiController) SearchWikis(c fiber.Ctx) error { Data: res, }) } + diff --git a/internal/dtos/request/snapshot.go b/internal/dtos/request/snapshot.go index b9d23d1..92f51e6 100644 --- a/internal/dtos/request/snapshot.go +++ b/internal/dtos/request/snapshot.go @@ -41,8 +41,8 @@ type EntitySnapshot struct { ID string `json:"id" validate:"required,uuidv7"` Source string `json:"source,omitempty" validate:"omitempty,oneof=inline ref"` Operation string `json:"operation,omitempty" validate:"omitempty,oneof=create update delete reference"` - Name string `json:"name,omitempty"` - Slug *string `json:"slug,omitempty"` + Name string `json:"name" validate:"required"` + Slug *string `json:"slug" validate:"omitempty,slug"` Description string `json:"description,omitempty"` Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1"` TimeStart *float64 `json:"time_start,omitempty"` @@ -79,12 +79,13 @@ type GeometryEntitySnapshot struct { } type WikiSnapshot struct { - ID string `json:"id" validate:"required,uuidv7"` - Source string `json:"source,omitempty" validate:"omitempty,oneof=inline ref"` - Operation string `json:"operation,omitempty" validate:"omitempty,oneof=create update delete reference"` - Title string `json:"title" validate:"required"` - Doc string `json:"doc,omitempty"` - UpdatedAt string `json:"updated_at,omitempty"` + ID string `json:"id" validate:"required,uuidv7"` + Source string `json:"source,omitempty" validate:"omitempty,oneof=inline ref"` + Operation string `json:"operation,omitempty" validate:"omitempty,oneof=create update delete reference"` + Title string `json:"title" validate:"required"` + Slug *string `json:"slug" validate:"omitempty,slug"` + Doc string `json:"doc,omitempty" validate:"omitempty"` + UpdatedAt string `json:"updated_at,omitempty"` } type EntityWikiLinkSnapshot struct { diff --git a/internal/dtos/response/wiki.go b/internal/dtos/response/wiki.go index 0618b20..b34b7dc 100644 --- a/internal/dtos/response/wiki.go +++ b/internal/dtos/response/wiki.go @@ -7,6 +7,7 @@ import ( type WikiResponse struct { ID string `json:"id"` Title string `json:"title,omitempty"` + Slug string `json:"slug,omitempty"` Content string `json:"content,omitempty"` ProjectID string `json:"project_id"` IsDeleted bool `json:"is_deleted,omitempty"` diff --git a/internal/gen/sqlc/entities.sql.go b/internal/gen/sqlc/entities.sql.go index cb56815..e32b3ca 100644 --- a/internal/gen/sqlc/entities.sql.go +++ b/internal/gen/sqlc/entities.sql.go @@ -181,6 +181,31 @@ func (q *Queries) GetEntityById(ctx context.Context, id pgtype.UUID) (Entity, er return i, err } +const getEntityBySlug = `-- name: GetEntityBySlug :one +SELECT id, project_id, name, slug, description, status, time_start, time_end, is_deleted, created_at, updated_at +FROM entities +WHERE slug = $1 AND is_deleted = false +` + +func (q *Queries) GetEntityBySlug(ctx context.Context, slug pgtype.Text) (Entity, error) { + row := q.db.QueryRow(ctx, getEntityBySlug, slug) + var i Entity + err := row.Scan( + &i.ID, + &i.ProjectID, + &i.Name, + &i.Slug, + &i.Description, + &i.Status, + &i.TimeStart, + &i.TimeEnd, + &i.IsDeleted, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const searchEntities = `-- name: SearchEntities :many SELECT id, project_id, name, slug, description, status, time_start, time_end, is_deleted, created_at, updated_at FROM entities diff --git a/internal/gen/sqlc/models.go b/internal/gen/sqlc/models.go index 5118547..86e7a60 100644 --- a/internal/gen/sqlc/models.go +++ b/internal/gen/sqlc/models.go @@ -183,6 +183,7 @@ type Wiki struct { ID pgtype.UUID `json:"id"` ProjectID pgtype.UUID `json:"project_id"` Title pgtype.Text `json:"title"` + Slug pgtype.Text `json:"slug"` Content pgtype.Text `json:"content"` IsDeleted bool `json:"is_deleted"` CreatedAt pgtype.Timestamptz `json:"created_at"` diff --git a/internal/gen/sqlc/wiki.sql.go b/internal/gen/sqlc/wiki.sql.go index 3601679..775754e 100644 --- a/internal/gen/sqlc/wiki.sql.go +++ b/internal/gen/sqlc/wiki.sql.go @@ -68,15 +68,16 @@ func (q *Queries) CreateEntityWikis(ctx context.Context, arg CreateEntityWikisPa const createWiki = `-- name: CreateWiki :one INSERT INTO wikis ( - id, title, content, project_id + id, title, slug, content, project_id ) VALUES ( - COALESCE($4::uuid, uuidv7()), $1, $2, $3 + COALESCE($5::uuid, uuidv7()), $1, $2, $3, $4 ) -RETURNING id, project_id, title, content, is_deleted, created_at, updated_at +RETURNING id, project_id, title, slug, content, is_deleted, created_at, updated_at ` type CreateWikiParams struct { Title pgtype.Text `json:"title"` + Slug pgtype.Text `json:"slug"` Content pgtype.Text `json:"content"` ProjectID pgtype.UUID `json:"project_id"` ID pgtype.UUID `json:"id"` @@ -85,6 +86,7 @@ type CreateWikiParams struct { func (q *Queries) CreateWiki(ctx context.Context, arg CreateWikiParams) (Wiki, error) { row := q.db.QueryRow(ctx, createWiki, arg.Title, + arg.Slug, arg.Content, arg.ProjectID, arg.ID, @@ -94,6 +96,7 @@ func (q *Queries) CreateWiki(ctx context.Context, arg CreateWikiParams) (Wiki, e &i.ID, &i.ProjectID, &i.Title, + &i.Slug, &i.Content, &i.IsDeleted, &i.CreatedAt, @@ -151,7 +154,7 @@ func (q *Queries) DeleteWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) } const getWikiById = `-- name: GetWikiById :one -SELECT id, project_id, title, content, is_deleted, created_at, updated_at +SELECT id, project_id, title, slug, content, is_deleted, created_at, updated_at FROM wikis WHERE id = $1 AND is_deleted = false ` @@ -163,6 +166,29 @@ func (q *Queries) GetWikiById(ctx context.Context, id pgtype.UUID) (Wiki, error) &i.ID, &i.ProjectID, &i.Title, + &i.Slug, + &i.Content, + &i.IsDeleted, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getWikiBySlug = `-- name: GetWikiBySlug :one +SELECT id, project_id, title, slug, content, is_deleted, created_at, updated_at +FROM wikis +WHERE slug = $1 AND is_deleted = false +` + +func (q *Queries) GetWikiBySlug(ctx context.Context, slug pgtype.Text) (Wiki, error) { + row := q.db.QueryRow(ctx, getWikiBySlug, slug) + var i Wiki + err := row.Scan( + &i.ID, + &i.ProjectID, + &i.Title, + &i.Slug, &i.Content, &i.IsDeleted, &i.CreatedAt, @@ -172,7 +198,7 @@ func (q *Queries) GetWikiById(ctx context.Context, id pgtype.UUID) (Wiki, error) } const getWikisByIDs = `-- name: GetWikisByIDs :many -SELECT id, project_id, title, content, is_deleted, created_at, updated_at FROM wikis WHERE id = ANY($1::uuid[]) AND is_deleted = false +SELECT id, project_id, title, slug, content, is_deleted, created_at, updated_at FROM wikis WHERE id = ANY($1::uuid[]) AND is_deleted = false ` func (q *Queries) GetWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Wiki, error) { @@ -188,6 +214,7 @@ func (q *Queries) GetWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([] &i.ID, &i.ProjectID, &i.Title, + &i.Slug, &i.Content, &i.IsDeleted, &i.CreatedAt, @@ -204,7 +231,7 @@ func (q *Queries) GetWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([] } const getWikisByProjectId = `-- name: GetWikisByProjectId :many -SELECT id, project_id, title, content, is_deleted, created_at, updated_at +SELECT id, project_id, title, slug, content, is_deleted, created_at, updated_at FROM wikis WHERE project_id = $1 AND is_deleted = false ` @@ -222,6 +249,7 @@ func (q *Queries) GetWikisByProjectId(ctx context.Context, projectID pgtype.UUID &i.ID, &i.ProjectID, &i.Title, + &i.Slug, &i.Content, &i.IsDeleted, &i.CreatedAt, @@ -238,7 +266,7 @@ func (q *Queries) GetWikisByProjectId(ctx context.Context, projectID pgtype.UUID } const searchWikis = `-- name: SearchWikis :many -SELECT w.id, w.project_id, w.title, w.content, w.is_deleted, w.created_at, w.updated_at +SELECT w.id, w.project_id, w.title, w.slug, w.content, w.is_deleted, w.created_at, w.updated_at FROM wikis w WHERE w.is_deleted = false AND ($1::uuid IS NULL OR w.project_id = $1::uuid) @@ -285,6 +313,7 @@ func (q *Queries) SearchWikis(ctx context.Context, arg SearchWikisParams) ([]Wik &i.ID, &i.ProjectID, &i.Title, + &i.Slug, &i.Content, &i.IsDeleted, &i.CreatedAt, @@ -304,14 +333,16 @@ const updateWiki = `-- name: UpdateWiki :one UPDATE wikis SET title = COALESCE($1, title), - content = COALESCE($2, content), - project_id = COALESCE($3, project_id) -WHERE id = $4 AND is_deleted = false -RETURNING id, project_id, title, content, is_deleted, created_at, updated_at + slug = COALESCE($2, slug), + content = COALESCE($3, content), + project_id = COALESCE($4, project_id) +WHERE id = $5 AND is_deleted = false +RETURNING id, project_id, title, slug, content, is_deleted, created_at, updated_at ` type UpdateWikiParams struct { Title pgtype.Text `json:"title"` + Slug pgtype.Text `json:"slug"` Content pgtype.Text `json:"content"` ProjectID pgtype.UUID `json:"project_id"` ID pgtype.UUID `json:"id"` @@ -320,6 +351,7 @@ type UpdateWikiParams struct { func (q *Queries) UpdateWiki(ctx context.Context, arg UpdateWikiParams) (Wiki, error) { row := q.db.QueryRow(ctx, updateWiki, arg.Title, + arg.Slug, arg.Content, arg.ProjectID, arg.ID, @@ -329,6 +361,7 @@ func (q *Queries) UpdateWiki(ctx context.Context, arg UpdateWikiParams) (Wiki, e &i.ID, &i.ProjectID, &i.Title, + &i.Slug, &i.Content, &i.IsDeleted, &i.CreatedAt, diff --git a/internal/models/wiki.go b/internal/models/wiki.go index 2595306..8c8900c 100644 --- a/internal/models/wiki.go +++ b/internal/models/wiki.go @@ -8,6 +8,7 @@ import ( type WikiEntity struct { ID string `json:"id"` Title string `json:"title"` + Slug string `json:"slug"` Content string `json:"content"` ProjectID string `json:"project_id"` IsDeleted bool `json:"is_deleted"` @@ -22,6 +23,7 @@ func (w *WikiEntity) ToResponse() *response.WikiResponse { return &response.WikiResponse{ ID: w.ID, Title: w.Title, + Slug: w.Slug, Content: w.Content, ProjectID: w.ProjectID, IsDeleted: w.IsDeleted, diff --git a/internal/repositories/entityRepository.go b/internal/repositories/entityRepository.go index 05f57d5..0c1f554 100644 --- a/internal/repositories/entityRepository.go +++ b/internal/repositories/entityRepository.go @@ -19,6 +19,7 @@ import ( type EntityRepository interface { GetByID(ctx context.Context, id pgtype.UUID) (*models.EntityEntity, error) GetByIDs(ctx context.Context, ids []string) ([]*models.EntityEntity, error) + GetBySlug(ctx context.Context, slug string) (*models.EntityEntity, error) Search(ctx context.Context, params sqlc.SearchEntitiesParams) ([]*models.EntityEntity, error) Create(ctx context.Context, params sqlc.CreateEntityParams) (*models.EntityEntity, error) Update(ctx context.Context, params sqlc.UpdateEntityParams) (*models.EntityEntity, error) @@ -83,17 +84,17 @@ func (r *entityRepository) getByIDsWithFallback(ctx context.Context, ids []strin if err == nil { for _, row := range dbRows { item := models.EntityEntity{ - ID: convert.UUIDToString(row.ID), - Name: row.Name, - Slug: convert.TextToString(row.Slug), - Description: convert.TextToString(row.Description), - ProjectID: convert.UUIDToString(row.ProjectID), - Status: convert.Int2ToInt16Ptr(row.Status), - TimeStart: convert.Int4ToPtr(row.TimeStart), - TimeEnd: convert.Int4ToPtr(row.TimeEnd), - IsDeleted: row.IsDeleted, - CreatedAt: convert.TimeToPtr(row.CreatedAt), - UpdatedAt: convert.TimeToPtr(row.UpdatedAt), + ID: convert.UUIDToString(row.ID), + Name: row.Name, + Slug: convert.TextToString(row.Slug), + Description: convert.TextToString(row.Description), + ProjectID: convert.UUIDToString(row.ProjectID), + Status: convert.Int2ToInt16Ptr(row.Status), + TimeStart: convert.Int4ToPtr(row.TimeStart), + TimeEnd: convert.Int4ToPtr(row.TimeEnd), + IsDeleted: row.IsDeleted, + CreatedAt: convert.TimeToPtr(row.CreatedAt), + UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } dbMap[item.ID] = &item } @@ -140,17 +141,17 @@ func (r *entityRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models } entity = models.EntityEntity{ - ID: convert.UUIDToString(row.ID), - Name: row.Name, - Slug: convert.TextToString(row.Slug), - Description: convert.TextToString(row.Description), - ProjectID: convert.UUIDToString(row.ProjectID), - Status: convert.Int2ToInt16Ptr(row.Status), - TimeStart: convert.Int4ToPtr(row.TimeStart), - TimeEnd: convert.Int4ToPtr(row.TimeEnd), - IsDeleted: row.IsDeleted, - CreatedAt: convert.TimeToPtr(row.CreatedAt), - UpdatedAt: convert.TimeToPtr(row.UpdatedAt), + ID: convert.UUIDToString(row.ID), + Name: row.Name, + Slug: convert.TextToString(row.Slug), + Description: convert.TextToString(row.Description), + ProjectID: convert.UUIDToString(row.ProjectID), + Status: convert.Int2ToInt16Ptr(row.Status), + TimeStart: convert.Int4ToPtr(row.TimeStart), + TimeEnd: convert.Int4ToPtr(row.TimeEnd), + IsDeleted: row.IsDeleted, + CreatedAt: convert.TimeToPtr(row.CreatedAt), + UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } _ = r.c.Set(ctx, cacheId, entity, constants.NormalCacheDuration) @@ -174,17 +175,17 @@ func (r *entityRepository) Search(ctx context.Context, params sqlc.SearchEntitie for _, row := range rows { entity := &models.EntityEntity{ - ID: convert.UUIDToString(row.ID), - Name: row.Name, - Slug: convert.TextToString(row.Slug), - Description: convert.TextToString(row.Description), - ProjectID: convert.UUIDToString(row.ProjectID), - Status: convert.Int2ToInt16Ptr(row.Status), - TimeStart: convert.Int4ToPtr(row.TimeStart), - TimeEnd: convert.Int4ToPtr(row.TimeEnd), - IsDeleted: row.IsDeleted, - CreatedAt: convert.TimeToPtr(row.CreatedAt), - UpdatedAt: convert.TimeToPtr(row.UpdatedAt), + ID: convert.UUIDToString(row.ID), + Name: row.Name, + Slug: convert.TextToString(row.Slug), + Description: convert.TextToString(row.Description), + ProjectID: convert.UUIDToString(row.ProjectID), + Status: convert.Int2ToInt16Ptr(row.Status), + TimeStart: convert.Int4ToPtr(row.TimeStart), + TimeEnd: convert.Int4ToPtr(row.TimeEnd), + IsDeleted: row.IsDeleted, + CreatedAt: convert.TimeToPtr(row.CreatedAt), + UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } ids = append(ids, entity.ID) entities = append(entities, entity) @@ -209,17 +210,17 @@ func (r *entityRepository) Create(ctx context.Context, params sqlc.CreateEntityP } entity := models.EntityEntity{ - ID: convert.UUIDToString(row.ID), - Name: row.Name, - Slug: convert.TextToString(row.Slug), - Description: convert.TextToString(row.Description), - ProjectID: convert.UUIDToString(row.ProjectID), - Status: convert.Int2ToInt16Ptr(row.Status), - TimeStart: convert.Int4ToPtr(row.TimeStart), - TimeEnd: convert.Int4ToPtr(row.TimeEnd), - IsDeleted: row.IsDeleted, - CreatedAt: convert.TimeToPtr(row.CreatedAt), - UpdatedAt: convert.TimeToPtr(row.UpdatedAt), + ID: convert.UUIDToString(row.ID), + Name: row.Name, + Slug: convert.TextToString(row.Slug), + Description: convert.TextToString(row.Description), + ProjectID: convert.UUIDToString(row.ProjectID), + Status: convert.Int2ToInt16Ptr(row.Status), + TimeStart: convert.Int4ToPtr(row.TimeStart), + TimeEnd: convert.Int4ToPtr(row.TimeEnd), + IsDeleted: row.IsDeleted, + CreatedAt: convert.TimeToPtr(row.CreatedAt), + UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } return &entity, nil @@ -231,19 +232,20 @@ func (r *entityRepository) Update(ctx context.Context, params sqlc.UpdateEntityP return nil, err } entity := models.EntityEntity{ - ID: convert.UUIDToString(row.ID), - Name: row.Name, - Slug: convert.TextToString(row.Slug), - Description: convert.TextToString(row.Description), - ProjectID: convert.UUIDToString(row.ProjectID), - Status: convert.Int2ToInt16Ptr(row.Status), - TimeStart: convert.Int4ToPtr(row.TimeStart), - TimeEnd: convert.Int4ToPtr(row.TimeEnd), - IsDeleted: row.IsDeleted, - CreatedAt: convert.TimeToPtr(row.CreatedAt), - UpdatedAt: convert.TimeToPtr(row.UpdatedAt), + ID: convert.UUIDToString(row.ID), + Name: row.Name, + Slug: convert.TextToString(row.Slug), + Description: convert.TextToString(row.Description), + ProjectID: convert.UUIDToString(row.ProjectID), + Status: convert.Int2ToInt16Ptr(row.Status), + TimeStart: convert.Int4ToPtr(row.TimeStart), + TimeEnd: convert.Int4ToPtr(row.TimeEnd), + IsDeleted: row.IsDeleted, + CreatedAt: convert.TimeToPtr(row.CreatedAt), + UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } _ = r.c.Del(ctx, fmt.Sprintf("entity:id:%s", entity.ID)) + _ = r.c.Del(ctx, fmt.Sprintf("entity:slug:%s", entity.Slug)) return &entity, nil } @@ -274,17 +276,17 @@ func (r *entityRepository) GetByProjectID(ctx context.Context, projectID pgtype. for _, row := range rows { entity := &models.EntityEntity{ - ID: convert.UUIDToString(row.ID), - Name: row.Name, - Slug: convert.TextToString(row.Slug), - Description: convert.TextToString(row.Description), - ProjectID: convert.UUIDToString(row.ProjectID), - Status: convert.Int2ToInt16Ptr(row.Status), - TimeStart: convert.Int4ToPtr(row.TimeStart), - TimeEnd: convert.Int4ToPtr(row.TimeEnd), - IsDeleted: row.IsDeleted, - CreatedAt: convert.TimeToPtr(row.CreatedAt), - UpdatedAt: convert.TimeToPtr(row.UpdatedAt), + ID: convert.UUIDToString(row.ID), + Name: row.Name, + Slug: convert.TextToString(row.Slug), + Description: convert.TextToString(row.Description), + ProjectID: convert.UUIDToString(row.ProjectID), + Status: convert.Int2ToInt16Ptr(row.Status), + TimeStart: convert.Int4ToPtr(row.TimeStart), + TimeEnd: convert.Int4ToPtr(row.TimeEnd), + IsDeleted: row.IsDeleted, + CreatedAt: convert.TimeToPtr(row.CreatedAt), + UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } ids = append(ids, entity.ID) entities = append(entities, entity) @@ -315,3 +317,35 @@ func (r *entityRepository) DeleteByIDs(ctx context.Context, ids []pgtype.UUID) e } return nil } + +func (r *entityRepository) GetBySlug(ctx context.Context, slug string) (*models.EntityEntity, error) { + cacheKey := fmt.Sprintf("entity:slug:%s", slug) + var entity models.EntityEntity + err := r.c.Get(ctx, cacheKey, &entity) + if err == nil { + _ = r.c.Set(ctx, cacheKey, entity, constants.NormalCacheDuration) + return &entity, nil + } + + row, err := r.q.GetEntityBySlug(ctx, convert.StringToText(slug)) + if err != nil { + return nil, err + } + + entity = models.EntityEntity{ + ID: convert.UUIDToString(row.ID), + Name: row.Name, + Slug: convert.TextToString(row.Slug), + Description: convert.TextToString(row.Description), + ProjectID: convert.UUIDToString(row.ProjectID), + Status: convert.Int2ToInt16Ptr(row.Status), + TimeStart: convert.Int4ToPtr(row.TimeStart), + TimeEnd: convert.Int4ToPtr(row.TimeEnd), + IsDeleted: row.IsDeleted, + CreatedAt: convert.TimeToPtr(row.CreatedAt), + UpdatedAt: convert.TimeToPtr(row.UpdatedAt), + } + _ = r.c.Set(ctx, cacheKey, entity, constants.NormalCacheDuration) + + return &entity, nil +} diff --git a/internal/repositories/wikiRepository.go b/internal/repositories/wikiRepository.go index 6aaab9b..c3e9a32 100644 --- a/internal/repositories/wikiRepository.go +++ b/internal/repositories/wikiRepository.go @@ -19,6 +19,7 @@ import ( type WikiRepository interface { GetByID(ctx context.Context, id pgtype.UUID) (*models.WikiEntity, error) GetByIDs(ctx context.Context, ids []string) ([]*models.WikiEntity, error) + GetBySlug(ctx context.Context, slug string) (*models.WikiEntity, error) Search(ctx context.Context, params sqlc.SearchWikisParams) ([]*models.WikiEntity, error) Create(ctx context.Context, params sqlc.CreateWikiParams) (*models.WikiEntity, error) Update(ctx context.Context, params sqlc.UpdateWikiParams) (*models.WikiEntity, error) @@ -90,6 +91,7 @@ func (r *wikiRepository) getByIDsWithFallback(ctx context.Context, ids []string) item := models.WikiEntity{ ID: convert.UUIDToString(row.ID), Title: convert.TextToString(row.Title), + Slug: convert.TextToString(row.Slug), Content: convert.TextToString(row.Content), IsDeleted: row.IsDeleted, ProjectID: convert.UUIDToString(row.ProjectID), @@ -143,8 +145,10 @@ func (r *wikiRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.W wiki = models.WikiEntity{ ID: convert.UUIDToString(row.ID), Title: convert.TextToString(row.Title), + Slug: convert.TextToString(row.Slug), Content: convert.TextToString(row.Content), IsDeleted: row.IsDeleted, + ProjectID: convert.UUIDToString(row.ProjectID), CreatedAt: convert.TimeToPtr(row.CreatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } @@ -172,8 +176,10 @@ func (r *wikiRepository) Search(ctx context.Context, params sqlc.SearchWikisPara wiki := &models.WikiEntity{ ID: convert.UUIDToString(row.ID), Title: convert.TextToString(row.Title), + Slug: convert.TextToString(row.Slug), Content: convert.TextToString(row.Content), IsDeleted: row.IsDeleted, + ProjectID: convert.UUIDToString(row.ProjectID), CreatedAt: convert.TimeToPtr(row.CreatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } @@ -201,8 +207,10 @@ func (r *wikiRepository) Create(ctx context.Context, params sqlc.CreateWikiParam wiki := models.WikiEntity{ ID: convert.UUIDToString(row.ID), Title: convert.TextToString(row.Title), + Slug: convert.TextToString(row.Slug), Content: convert.TextToString(row.Content), IsDeleted: row.IsDeleted, + ProjectID: convert.UUIDToString(row.ProjectID), CreatedAt: convert.TimeToPtr(row.CreatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } @@ -218,12 +226,15 @@ func (r *wikiRepository) Update(ctx context.Context, params sqlc.UpdateWikiParam wiki := models.WikiEntity{ ID: convert.UUIDToString(row.ID), Title: convert.TextToString(row.Title), + Slug: convert.TextToString(row.Slug), Content: convert.TextToString(row.Content), IsDeleted: row.IsDeleted, + ProjectID: convert.UUIDToString(row.ProjectID), CreatedAt: convert.TimeToPtr(row.CreatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt), } _ = r.c.Del(ctx, fmt.Sprintf("wiki:id:%s", wiki.ID)) + _ = r.c.Del(ctx, fmt.Sprintf("wiki:slug:%s", wiki.Slug)) return &wiki, nil } @@ -272,6 +283,7 @@ func (r *wikiRepository) GetByProjectID(ctx context.Context, projectID pgtype.UU wiki := &models.WikiEntity{ ID: convert.UUIDToString(row.ID), Title: convert.TextToString(row.Title), + Slug: convert.TextToString(row.Slug), Content: convert.TextToString(row.Content), IsDeleted: row.IsDeleted, ProjectID: convert.UUIDToString(row.ProjectID), @@ -318,3 +330,32 @@ func (r *wikiRepository) DeleteEntityWiki(ctx context.Context, entityID pgtype.U WikiID: wikiID, }) } + +func (r *wikiRepository) GetBySlug(ctx context.Context, slug string) (*models.WikiEntity, error) { + cacheKey := fmt.Sprintf("wiki:slug:%s", slug) + var wiki models.WikiEntity + err := r.c.Get(ctx, cacheKey, &wiki) + if err == nil { + _ = r.c.Set(ctx, cacheKey, wiki, constants.NormalCacheDuration) + return &wiki, nil + } + + row, err := r.q.GetWikiBySlug(ctx, convert.StringToText(slug)) + if err != nil { + return nil, err + } + + wiki = models.WikiEntity{ + ID: convert.UUIDToString(row.ID), + Title: convert.TextToString(row.Title), + Slug: convert.TextToString(row.Slug), + Content: convert.TextToString(row.Content), + IsDeleted: row.IsDeleted, + ProjectID: convert.UUIDToString(row.ProjectID), + CreatedAt: convert.TimeToPtr(row.CreatedAt), + UpdatedAt: convert.TimeToPtr(row.UpdatedAt), + } + _ = r.c.Set(ctx, cacheKey, wiki, constants.NormalCacheDuration) + + return &wiki, nil +} diff --git a/internal/routes/entityRoute.go b/internal/routes/entityRoute.go index 97c24ab..c36a214 100644 --- a/internal/routes/entityRoute.go +++ b/internal/routes/entityRoute.go @@ -9,5 +9,8 @@ import ( func EntityRoutes(router fiber.Router, entityController *controllers.EntityController) { entity := router.Group("/entities") entity.Get("/", entityController.SearchEntities) + entity.Get("/slug/exists", entityController.IsExistEntitySlug) + entity.Get("/slug/:slug", entityController.GetEntityBySlug) entity.Get("/:id", entityController.GetEntityById) } + diff --git a/internal/routes/wikiRoute.go b/internal/routes/wikiRoute.go index 3f77ae1..6a3dcfc 100644 --- a/internal/routes/wikiRoute.go +++ b/internal/routes/wikiRoute.go @@ -9,5 +9,8 @@ import ( func WikiRoutes(router fiber.Router, wikiController *controllers.WikiController) { wiki := router.Group("/wikis") wiki.Get("/", wikiController.SearchWikis) + wiki.Get("/slug/exists", wikiController.IsExistWikiSlug) + wiki.Get("/slug/:slug", wikiController.GetWikiBySlug) wiki.Get("/:id", wikiController.GetWikiById) } + diff --git a/internal/services/entityService.go b/internal/services/entityService.go index acb5a70..ac3d03d 100644 --- a/internal/services/entityService.go +++ b/internal/services/entityService.go @@ -2,6 +2,8 @@ package services import ( "context" + "database/sql" + "errors" "history-api/internal/dtos/request" "history-api/internal/dtos/response" "history-api/internal/gen/sqlc" @@ -14,6 +16,8 @@ import ( type EntityService interface { GetEntityByID(ctx context.Context, id string) (*response.EntityResponse, *fiber.Error) + GetEntityBySlug(ctx context.Context, slug string) (*response.EntityResponse, *fiber.Error) + IsExistEntitySlug(ctx context.Context, slug string) (bool, *fiber.Error) SearchEntities(ctx context.Context, req *request.SearchEntityDto) ([]*response.EntityResponse, *fiber.Error) } @@ -40,6 +44,29 @@ func (s *entityService) GetEntityByID(ctx context.Context, id string) (*response return entity.ToResponse(), nil } +func (s *entityService) GetEntityBySlug(ctx context.Context, slug string) (*response.EntityResponse, *fiber.Error) { + if slug == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "Slug is required") + } + entity, err := s.entityRepo.GetBySlug(ctx, slug) + if err != nil { + return nil, fiber.NewError(fiber.StatusNotFound, "Entity not found") + } + + return entity.ToResponse(), nil +} + +func (s *entityService) IsExistEntitySlug(ctx context.Context, slug string) (bool, *fiber.Error) { + if slug == "" { + return false, fiber.NewError(fiber.StatusBadRequest, "Slug is required") + } + entity, err := s.entityRepo.GetBySlug(ctx, slug) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return false, fiber.NewError(fiber.StatusInternalServerError, "Failed to check slug existence") + } + return entity != nil, nil +} + func (s *entityService) SearchEntities(ctx context.Context, req *request.SearchEntityDto) ([]*response.EntityResponse, *fiber.Error) { limit := int32(25) if req.Limit > 0 { diff --git a/internal/services/submissionService.go b/internal/services/submissionService.go index 777f961..6df7028 100644 --- a/internal/services/submissionService.go +++ b/internal/services/submissionService.go @@ -2,7 +2,9 @@ package services import ( "context" + "database/sql" "encoding/json" + "errors" "fmt" "history-api/internal/dtos/request" "history-api/internal/dtos/response" @@ -95,6 +97,35 @@ func (s *submissionService) CreateSubmission(ctx context.Context, userID string, return nil, fiber.NewError(fiber.StatusBadRequest, "Commit does not belong to project") } + var snapshotData request.CommitSnapshot + if err := json.Unmarshal(commit.SnapshotJson, &snapshotData); err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to parse commit snapshot") + } + + for _, entity := range snapshotData.Entities { + if entity.Slug != nil { + exist, err := s.entityRepo.GetBySlug(ctx, *entity.Slug) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get entity") + } + if exist != nil { + return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Entity %s already exists", *entity.Slug)) + } + } + } + + for _, wiki := range snapshotData.Wikis { + if wiki.Slug != nil { + exist, err := s.wikiRepo.GetBySlug(ctx, *wiki.Slug) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get wiki") + } + if exist != nil { + return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Wiki %s already exists", *wiki.Slug)) + } + } + } + project, err := s.projectRepo.GetByID(ctx, projectUUID) if err != nil { return nil, fiber.NewError(fiber.StatusNotFound, "Project not found") @@ -178,12 +209,12 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer listDeleteWikis := make([]pgtype.UUID, 0) listDeleteGeometries := make([]pgtype.UUID, 0) var snapshotData request.CommitSnapshot - if status == constants.StatusTypeApproved { - err = json.Unmarshal(commit.SnapshotJson, &snapshotData) - if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to parse commit snapshot") - } + err = json.Unmarshal(commit.SnapshotJson, &snapshotData) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to parse commit snapshot") + } + if status == constants.StatusTypeApproved { projectUUID, err := convert.StringToUUID(commit.ProjectID) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID") @@ -312,7 +343,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer }) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update entity: "+entity.ID) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update entity: "+err.Error()) } newEntities = append(newEntities, snapshotData.Entities[i]) @@ -330,7 +361,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer }) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create entity: "+entity.ID) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create entity: "+err.Error()) } newEntities = append(newEntities, snapshotData.Entities[i]) @@ -390,7 +421,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer _, err := geometryRepo.Update(ctx, params) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update geometry: "+geo.ID) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update geometry: "+err.Error()) } newGeometries = append(newGeometries, snapshotData.Geometries[i]) @@ -413,7 +444,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer _, err := geometryRepo.Create(ctx, params) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create geometry: "+geo.ID) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create geometry: "+err.Error()) } newGeometries = append(newGeometries, snapshotData.Geometries[i]) @@ -453,11 +484,12 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer _, err := wikiRepo.Update(ctx, sqlc.UpdateWikiParams{ ID: wikiUUID, Title: convert.StringToText(wiki.Title), + Slug: convert.PtrToText(wiki.Slug), Content: convert.StringToText(wiki.Doc), ProjectID: projectUUID, }) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update wiki: "+wiki.ID) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update wiki: "+err.Error()) } newWikis = append(newWikis, snapshotData.Wikis[i]) @@ -465,11 +497,12 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer _, err := wikiRepo.Create(ctx, sqlc.CreateWikiParams{ ID: wikiUUID, Title: convert.StringToText(wiki.Title), + Slug: convert.PtrToText(wiki.Slug), Content: convert.StringToText(wiki.Doc), ProjectID: projectUUID, }) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki: "+wiki.ID) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki: "+err.Error()) } newWikis = append(newWikis, snapshotData.Wikis[i]) diff --git a/internal/services/wikiService.go b/internal/services/wikiService.go index 69dc48c..2b902c8 100644 --- a/internal/services/wikiService.go +++ b/internal/services/wikiService.go @@ -2,6 +2,8 @@ package services import ( "context" + "database/sql" + "errors" "history-api/internal/dtos/request" "history-api/internal/dtos/response" "history-api/internal/gen/sqlc" @@ -14,6 +16,8 @@ import ( type WikiService interface { GetWikiByID(ctx context.Context, id string) (*response.WikiResponse, *fiber.Error) + GetWikiBySlug(ctx context.Context, slug string) (*response.WikiResponse, *fiber.Error) + IsExistWikiSlug(ctx context.Context, slug string) (bool, *fiber.Error) SearchWikis(ctx context.Context, req *request.SearchWikiDto) ([]*response.WikiResponse, *fiber.Error) } @@ -40,6 +44,29 @@ func (s *wikiService) GetWikiByID(ctx context.Context, id string) (*response.Wik return wiki.ToResponse(), nil } +func (s *wikiService) GetWikiBySlug(ctx context.Context, slug string) (*response.WikiResponse, *fiber.Error) { + if slug == "" { + return nil, fiber.NewError(fiber.StatusBadRequest, "Slug is required") + } + wiki, err := s.wikiRepo.GetBySlug(ctx, slug) + if err != nil { + return nil, fiber.NewError(fiber.StatusNotFound, "Wiki not found") + } + + return wiki.ToResponse(), nil +} + +func (s *wikiService) IsExistWikiSlug(ctx context.Context, slug string) (bool, *fiber.Error) { + if slug == "" { + return false, fiber.NewError(fiber.StatusBadRequest, "Slug is required") + } + wiki, err := s.wikiRepo.GetBySlug(ctx, slug) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return false, fiber.NewError(fiber.StatusInternalServerError, "Failed to check slug existence") + } + return wiki != nil, nil +} + func (s *wikiService) SearchWikis(ctx context.Context, req *request.SearchWikiDto) ([]*response.WikiResponse, *fiber.Error) { limit := int32(25) if req.Limit > 0 { diff --git a/pkg/constants/regex.go b/pkg/constants/regex.go index 2c576c0..ba2a536 100644 --- a/pkg/constants/regex.go +++ b/pkg/constants/regex.go @@ -17,6 +17,7 @@ var ( EMAIL_REGEX = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) YOUTUBE_VIDEO_ID_REGEX = regexp.MustCompile(`(?:\/|v=|\/v\/|embed\/|watch\?v=|watch\?.+&v=)([\w-]{11})`) BANK_INPUT = regexp.MustCompile(`[__]{2,}`) + SLUG_REGEX = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`) ) func ValidatePassword(password string) error { @@ -36,4 +37,4 @@ func ValidatePassword(password string) error { return errors.New("password must contain at least one special character") } return nil -} \ No newline at end of file +} diff --git a/pkg/validator/validator.go b/pkg/validator/validator.go index 70fc655..cbb60a4 100644 --- a/pkg/validator/validator.go +++ b/pkg/validator/validator.go @@ -7,6 +7,8 @@ import ( "reflect" "strings" + "history-api/pkg/constants" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v3" "github.com/google/uuid" @@ -53,6 +55,14 @@ func init() { } return u.Version() == 7 }) + + validate.RegisterValidation("slug", func(fl validator.FieldLevel) bool { + val := fl.Field().String() + if val == "" { + return true + } + return constants.SLUG_REGEX.MatchString(val) + }) } func isValidURL(s string) bool {