UPDATE: new db
All checks were successful
Build and Release / release (push) Successful in 1m30s

This commit is contained in:
2026-05-05 16:57:44 +07:00
parent 8b440ad5c8
commit 29944915cd
25 changed files with 893 additions and 110 deletions

View File

@@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS entities (
id UUID PRIMARY KEY DEFAULT uuidv7(), id UUID PRIMARY KEY DEFAULT uuidv7(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name TEXT NOT NULL, name TEXT NOT NULL,
slug TEXT, slug TEXT UNIQUE,
description TEXT, description TEXT,
status SMALLINT, status SMALLINT,
time_start INT, time_start INT,
@@ -12,6 +12,9 @@ CREATE TABLE IF NOT EXISTS entities (
updated_at TIMESTAMPTZ DEFAULT now() 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 CREATE INDEX idx_entities_name_search
ON entities USING GIN (name gin_trgm_ops); ON entities USING GIN (name gin_trgm_ops);

View File

@@ -4,12 +4,16 @@ CREATE TABLE IF NOT EXISTS wikis (
id UUID PRIMARY KEY DEFAULT uuidv7(), id UUID PRIMARY KEY DEFAULT uuidv7(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
title TEXT, title TEXT,
slug TEXT,
content TEXT, content TEXT,
is_deleted BOOLEAN NOT NULL DEFAULT false, is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(), created_at TIMESTAMPTZ DEFAULT now(),
updated_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 ( CREATE TABLE IF NOT EXISTS entity_wikis (
entity_id UUID REFERENCES entities(id) ON DELETE CASCADE, entity_id UUID REFERENCES entities(id) ON DELETE CASCADE,

View File

@@ -60,3 +60,8 @@ WHERE project_id = $1 AND is_deleted = false;
UPDATE entities UPDATE entities
SET is_deleted = true SET is_deleted = true
WHERE id = ANY($1::uuid[]); WHERE id = ANY($1::uuid[]);
-- name: GetEntityBySlug :one
SELECT *
FROM entities
WHERE slug = $1 AND is_deleted = false;

View File

@@ -1,8 +1,8 @@
-- name: CreateWiki :one -- name: CreateWiki :one
INSERT INTO wikis ( INSERT INTO wikis (
id, title, content, project_id id, title, slug, content, project_id
) VALUES ( ) VALUES (
COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3 COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3, $4
) )
RETURNING *; RETURNING *;
@@ -15,6 +15,7 @@ WHERE id = $1 AND is_deleted = false;
UPDATE wikis UPDATE wikis
SET SET
title = COALESCE(sqlc.narg('title'), title), title = COALESCE(sqlc.narg('title'), title),
slug = COALESCE(sqlc.narg('slug'), slug),
content = COALESCE(sqlc.narg('content'), content), content = COALESCE(sqlc.narg('content'), content),
project_id = COALESCE(sqlc.narg('project_id'), project_id) project_id = COALESCE(sqlc.narg('project_id'), project_id)
WHERE id = sqlc.arg('id') AND is_deleted = false WHERE id = sqlc.arg('id') AND is_deleted = false
@@ -84,3 +85,8 @@ WHERE wiki_id = $1;
-- name: DeleteEntityWiki :exec -- name: DeleteEntityWiki :exec
DELETE FROM entity_wikis DELETE FROM entity_wikis
WHERE entity_id = $1 AND wiki_id = $2; WHERE entity_id = $1 AND wiki_id = $2;
-- name: GetWikiBySlug :one
SELECT *
FROM wikis
WHERE slug = $1 AND is_deleted = false;

View File

@@ -87,7 +87,7 @@ CREATE TABLE IF NOT EXISTS entities (
id UUID PRIMARY KEY DEFAULT uuidv7(), id UUID PRIMARY KEY DEFAULT uuidv7(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
name TEXT NOT NULL, name TEXT NOT NULL,
slug TEXT, slug TEXT UNIQUE,
description TEXT, description TEXT,
status SMALLINT, status SMALLINT,
time_start INT, time_start INT,
@@ -101,6 +101,7 @@ CREATE TABLE IF NOT EXISTS wikis (
id UUID PRIMARY KEY DEFAULT uuidv7(), id UUID PRIMARY KEY DEFAULT uuidv7(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
title TEXT, title TEXT,
slug TEXT UNIQUE,
content TEXT, content TEXT,
is_deleted BOOLEAN NOT NULL DEFAULT false, is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(), created_at TIMESTAMPTZ DEFAULT now(),
@@ -114,8 +115,6 @@ CREATE TABLE IF NOT EXISTS entity_wikis (
PRIMARY KEY (entity_id, wiki_id) PRIMARY KEY (entity_id, wiki_id)
); );
CREATE INDEX idx_entity_wikis_project_id ON entity_wikis(project_id);
CREATE TABLE IF NOT EXISTS geometries ( CREATE TABLE IF NOT EXISTS geometries (
id UUID PRIMARY KEY DEFAULT uuidv7(), id UUID PRIMARY KEY DEFAULT uuidv7(),
geo_type SMALLINT NOT NULL DEFAULT 1, geo_type SMALLINT NOT NULL DEFAULT 1,
@@ -137,8 +136,6 @@ CREATE TABLE IF NOT EXISTS entity_geometries (
PRIMARY KEY (entity_id, geometry_id) PRIMARY KEY (entity_id, geometry_id)
); );
CREATE INDEX idx_entity_geometries_project_id ON entity_geometries(project_id);
CREATE TABLE IF NOT EXISTS commits ( CREATE TABLE IF NOT EXISTS commits (
id UUID PRIMARY KEY DEFAULT uuidv7(), id UUID PRIMARY KEY DEFAULT uuidv7(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,

View File

@@ -403,7 +403,7 @@ const docTemplate = `{
"post": { "post": {
"security": [ "security": [
{ {
"ApiKeyAuth": [] "BearerAuth": []
} }
], ],
"description": "Ask a history question based on project context or global knowledge using RAG", "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}": { "/entities/{id}": {
"get": { "get": {
"description": "Get detailed information about a specific entity", "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}": { "/wikis/{id}": {
"get": { "get": {
"description": "Get detailed information about a specific wiki", "description": "Get detailed information about a specific wiki",
@@ -3847,7 +3999,8 @@ const docTemplate = `{
"history-api_internal_dtos_request.EntitySnapshot": { "history-api_internal_dtos_request.EntitySnapshot": {
"type": "object", "type": "object",
"required": [ "required": [
"id" "id",
"name"
], ],
"properties": { "properties": {
"base_hash": { "base_hash": {
@@ -4360,6 +4513,9 @@ const docTemplate = `{
"reference" "reference"
] ]
}, },
"slug": {
"type": "string"
},
"source": { "source": {
"type": "string", "type": "string",
"enum": [ "enum": [

View File

@@ -396,7 +396,7 @@
"post": { "post": {
"security": [ "security": [
{ {
"ApiKeyAuth": [] "BearerAuth": []
} }
], ],
"description": "Ask a history question based on project context or global knowledge using RAG", "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}": { "/entities/{id}": {
"get": { "get": {
"description": "Get detailed information about a specific entity", "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}": { "/wikis/{id}": {
"get": { "get": {
"description": "Get detailed information about a specific wiki", "description": "Get detailed information about a specific wiki",
@@ -3840,7 +3992,8 @@
"history-api_internal_dtos_request.EntitySnapshot": { "history-api_internal_dtos_request.EntitySnapshot": {
"type": "object", "type": "object",
"required": [ "required": [
"id" "id",
"name"
], ],
"properties": { "properties": {
"base_hash": { "base_hash": {
@@ -4353,6 +4506,9 @@
"reference" "reference"
] ]
}, },
"slug": {
"type": "string"
},
"source": { "source": {
"type": "string", "type": "string",
"enum": [ "enum": [

View File

@@ -232,6 +232,7 @@ definitions:
type: number type: number
required: required:
- id - id
- name
type: object type: object
history-api_internal_dtos_request.EntityWikiLinkSnapshot: history-api_internal_dtos_request.EntityWikiLinkSnapshot:
properties: properties:
@@ -550,6 +551,8 @@ definitions:
- delete - delete
- reference - reference
type: string type: string
slug:
type: string
source: source:
enum: enum:
- inline - inline
@@ -899,7 +902,7 @@ paths:
schema: schema:
$ref: '#/definitions/history-api_internal_dtos_response.CommonResponse' $ref: '#/definitions/history-api_internal_dtos_response.CommonResponse'
security: security:
- ApiKeyAuth: [] - BearerAuth: []
summary: Ask the AI chatbot summary: Ask the AI chatbot
tags: tags:
- Chatbot - Chatbot
@@ -963,6 +966,57 @@ paths:
summary: Get entity by ID summary: Get entity by ID
tags: tags:
- Entities - 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: /geometries:
get: get:
consumes: consumes:
@@ -2902,6 +2956,56 @@ paths:
summary: Get wiki by ID summary: Get wiki by ID
tags: tags:
- Wikis - 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: securityDefinitions:
BearerAuth: BearerAuth:
description: Type "Bearer " followed by a space and JWT token. description: Type "Bearer " followed by a space and JWT token.

View File

@@ -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. // SearchEntities handles searching for entities.
// @Summary Search entities // @Summary Search entities
// @Description Search entities with cursor pagination // @Description Search entities with cursor pagination
@@ -81,3 +135,4 @@ func (h *EntityController) SearchEntities(c fiber.Ctx) error {
Data: res, Data: res,
}) })
} }

View File

@@ -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. // SearchWikis handles searching for wikis.
// @Summary Search wikis // @Summary Search wikis
// @Description Search wikis with cursor pagination // @Description Search wikis with cursor pagination
@@ -81,3 +135,4 @@ func (h *WikiController) SearchWikis(c fiber.Ctx) error {
Data: res, Data: res,
}) })
} }

View File

@@ -41,8 +41,8 @@ type EntitySnapshot struct {
ID string `json:"id" validate:"required,uuidv7"` ID string `json:"id" validate:"required,uuidv7"`
Source string `json:"source,omitempty" validate:"omitempty,oneof=inline ref"` Source string `json:"source,omitempty" validate:"omitempty,oneof=inline ref"`
Operation string `json:"operation,omitempty" validate:"omitempty,oneof=create update delete reference"` Operation string `json:"operation,omitempty" validate:"omitempty,oneof=create update delete reference"`
Name string `json:"name,omitempty"` Name string `json:"name" validate:"required"`
Slug *string `json:"slug,omitempty"` Slug *string `json:"slug" validate:"omitempty,slug"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1"` Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1"`
TimeStart *float64 `json:"time_start,omitempty"` TimeStart *float64 `json:"time_start,omitempty"`
@@ -79,12 +79,13 @@ type GeometryEntitySnapshot struct {
} }
type WikiSnapshot struct { type WikiSnapshot struct {
ID string `json:"id" validate:"required,uuidv7"` ID string `json:"id" validate:"required,uuidv7"`
Source string `json:"source,omitempty" validate:"omitempty,oneof=inline ref"` Source string `json:"source,omitempty" validate:"omitempty,oneof=inline ref"`
Operation string `json:"operation,omitempty" validate:"omitempty,oneof=create update delete reference"` Operation string `json:"operation,omitempty" validate:"omitempty,oneof=create update delete reference"`
Title string `json:"title" validate:"required"` Title string `json:"title" validate:"required"`
Doc string `json:"doc,omitempty"` Slug *string `json:"slug" validate:"omitempty,slug"`
UpdatedAt string `json:"updated_at,omitempty"` Doc string `json:"doc,omitempty" validate:"omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
} }
type EntityWikiLinkSnapshot struct { type EntityWikiLinkSnapshot struct {

View File

@@ -7,6 +7,7 @@ import (
type WikiResponse struct { type WikiResponse struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Slug string `json:"slug,omitempty"`
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
ProjectID string `json:"project_id"` ProjectID string `json:"project_id"`
IsDeleted bool `json:"is_deleted,omitempty"` IsDeleted bool `json:"is_deleted,omitempty"`

View File

@@ -181,6 +181,31 @@ func (q *Queries) GetEntityById(ctx context.Context, id pgtype.UUID) (Entity, er
return i, err 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 const searchEntities = `-- name: SearchEntities :many
SELECT id, project_id, name, slug, description, status, time_start, time_end, is_deleted, created_at, updated_at SELECT id, project_id, name, slug, description, status, time_start, time_end, is_deleted, created_at, updated_at
FROM entities FROM entities

View File

@@ -183,6 +183,7 @@ type Wiki struct {
ID pgtype.UUID `json:"id"` ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"` ProjectID pgtype.UUID `json:"project_id"`
Title pgtype.Text `json:"title"` Title pgtype.Text `json:"title"`
Slug pgtype.Text `json:"slug"`
Content pgtype.Text `json:"content"` Content pgtype.Text `json:"content"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`

View File

@@ -68,15 +68,16 @@ func (q *Queries) CreateEntityWikis(ctx context.Context, arg CreateEntityWikisPa
const createWiki = `-- name: CreateWiki :one const createWiki = `-- name: CreateWiki :one
INSERT INTO wikis ( INSERT INTO wikis (
id, title, content, project_id id, title, slug, content, project_id
) VALUES ( ) 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 { type CreateWikiParams struct {
Title pgtype.Text `json:"title"` Title pgtype.Text `json:"title"`
Slug pgtype.Text `json:"slug"`
Content pgtype.Text `json:"content"` Content pgtype.Text `json:"content"`
ProjectID pgtype.UUID `json:"project_id"` ProjectID pgtype.UUID `json:"project_id"`
ID pgtype.UUID `json:"id"` ID pgtype.UUID `json:"id"`
@@ -85,6 +86,7 @@ type CreateWikiParams struct {
func (q *Queries) CreateWiki(ctx context.Context, arg CreateWikiParams) (Wiki, error) { func (q *Queries) CreateWiki(ctx context.Context, arg CreateWikiParams) (Wiki, error) {
row := q.db.QueryRow(ctx, createWiki, row := q.db.QueryRow(ctx, createWiki,
arg.Title, arg.Title,
arg.Slug,
arg.Content, arg.Content,
arg.ProjectID, arg.ProjectID,
arg.ID, arg.ID,
@@ -94,6 +96,7 @@ func (q *Queries) CreateWiki(ctx context.Context, arg CreateWikiParams) (Wiki, e
&i.ID, &i.ID,
&i.ProjectID, &i.ProjectID,
&i.Title, &i.Title,
&i.Slug,
&i.Content, &i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
@@ -151,7 +154,7 @@ func (q *Queries) DeleteWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID)
} }
const getWikiById = `-- name: GetWikiById :one 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 FROM wikis
WHERE id = $1 AND is_deleted = false 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.ID,
&i.ProjectID, &i.ProjectID,
&i.Title, &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.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
@@ -172,7 +198,7 @@ func (q *Queries) GetWikiById(ctx context.Context, id pgtype.UUID) (Wiki, error)
} }
const getWikisByIDs = `-- name: GetWikisByIDs :many 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) { 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.ID,
&i.ProjectID, &i.ProjectID,
&i.Title, &i.Title,
&i.Slug,
&i.Content, &i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
@@ -204,7 +231,7 @@ func (q *Queries) GetWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]
} }
const getWikisByProjectId = `-- name: GetWikisByProjectId :many 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 FROM wikis
WHERE project_id = $1 AND is_deleted = false 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.ID,
&i.ProjectID, &i.ProjectID,
&i.Title, &i.Title,
&i.Slug,
&i.Content, &i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
@@ -238,7 +266,7 @@ func (q *Queries) GetWikisByProjectId(ctx context.Context, projectID pgtype.UUID
} }
const searchWikis = `-- name: SearchWikis :many 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 FROM wikis w
WHERE w.is_deleted = false WHERE w.is_deleted = false
AND ($1::uuid IS NULL OR w.project_id = $1::uuid) 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.ID,
&i.ProjectID, &i.ProjectID,
&i.Title, &i.Title,
&i.Slug,
&i.Content, &i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
@@ -304,14 +333,16 @@ const updateWiki = `-- name: UpdateWiki :one
UPDATE wikis UPDATE wikis
SET SET
title = COALESCE($1, title), title = COALESCE($1, title),
content = COALESCE($2, content), slug = COALESCE($2, slug),
project_id = COALESCE($3, project_id) content = COALESCE($3, content),
WHERE id = $4 AND is_deleted = false project_id = COALESCE($4, project_id)
RETURNING id, project_id, title, content, is_deleted, created_at, updated_at WHERE id = $5 AND is_deleted = false
RETURNING id, project_id, title, slug, content, is_deleted, created_at, updated_at
` `
type UpdateWikiParams struct { type UpdateWikiParams struct {
Title pgtype.Text `json:"title"` Title pgtype.Text `json:"title"`
Slug pgtype.Text `json:"slug"`
Content pgtype.Text `json:"content"` Content pgtype.Text `json:"content"`
ProjectID pgtype.UUID `json:"project_id"` ProjectID pgtype.UUID `json:"project_id"`
ID pgtype.UUID `json:"id"` ID pgtype.UUID `json:"id"`
@@ -320,6 +351,7 @@ type UpdateWikiParams struct {
func (q *Queries) UpdateWiki(ctx context.Context, arg UpdateWikiParams) (Wiki, error) { func (q *Queries) UpdateWiki(ctx context.Context, arg UpdateWikiParams) (Wiki, error) {
row := q.db.QueryRow(ctx, updateWiki, row := q.db.QueryRow(ctx, updateWiki,
arg.Title, arg.Title,
arg.Slug,
arg.Content, arg.Content,
arg.ProjectID, arg.ProjectID,
arg.ID, arg.ID,
@@ -329,6 +361,7 @@ func (q *Queries) UpdateWiki(ctx context.Context, arg UpdateWikiParams) (Wiki, e
&i.ID, &i.ID,
&i.ProjectID, &i.ProjectID,
&i.Title, &i.Title,
&i.Slug,
&i.Content, &i.Content,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,

View File

@@ -8,6 +8,7 @@ import (
type WikiEntity struct { type WikiEntity struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` Title string `json:"title"`
Slug string `json:"slug"`
Content string `json:"content"` Content string `json:"content"`
ProjectID string `json:"project_id"` ProjectID string `json:"project_id"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
@@ -22,6 +23,7 @@ func (w *WikiEntity) ToResponse() *response.WikiResponse {
return &response.WikiResponse{ return &response.WikiResponse{
ID: w.ID, ID: w.ID,
Title: w.Title, Title: w.Title,
Slug: w.Slug,
Content: w.Content, Content: w.Content,
ProjectID: w.ProjectID, ProjectID: w.ProjectID,
IsDeleted: w.IsDeleted, IsDeleted: w.IsDeleted,

View File

@@ -19,6 +19,7 @@ import (
type EntityRepository interface { type EntityRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.EntityEntity, error) GetByID(ctx context.Context, id pgtype.UUID) (*models.EntityEntity, error)
GetByIDs(ctx context.Context, ids []string) ([]*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) Search(ctx context.Context, params sqlc.SearchEntitiesParams) ([]*models.EntityEntity, error)
Create(ctx context.Context, params sqlc.CreateEntityParams) (*models.EntityEntity, error) Create(ctx context.Context, params sqlc.CreateEntityParams) (*models.EntityEntity, error)
Update(ctx context.Context, params sqlc.UpdateEntityParams) (*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 { if err == nil {
for _, row := range dbRows { for _, row := range dbRows {
item := models.EntityEntity{ item := models.EntityEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Name: row.Name, Name: row.Name,
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Description: convert.TextToString(row.Description), Description: convert.TextToString(row.Description),
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status), Status: convert.Int2ToInt16Ptr(row.Status),
TimeStart: convert.Int4ToPtr(row.TimeStart), TimeStart: convert.Int4ToPtr(row.TimeStart),
TimeEnd: convert.Int4ToPtr(row.TimeEnd), TimeEnd: convert.Int4ToPtr(row.TimeEnd),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
} }
dbMap[item.ID] = &item dbMap[item.ID] = &item
} }
@@ -140,17 +141,17 @@ func (r *entityRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models
} }
entity = models.EntityEntity{ entity = models.EntityEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Name: row.Name, Name: row.Name,
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Description: convert.TextToString(row.Description), Description: convert.TextToString(row.Description),
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status), Status: convert.Int2ToInt16Ptr(row.Status),
TimeStart: convert.Int4ToPtr(row.TimeStart), TimeStart: convert.Int4ToPtr(row.TimeStart),
TimeEnd: convert.Int4ToPtr(row.TimeEnd), TimeEnd: convert.Int4ToPtr(row.TimeEnd),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
} }
_ = r.c.Set(ctx, cacheId, entity, constants.NormalCacheDuration) _ = 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 { for _, row := range rows {
entity := &models.EntityEntity{ entity := &models.EntityEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Name: row.Name, Name: row.Name,
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Description: convert.TextToString(row.Description), Description: convert.TextToString(row.Description),
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status), Status: convert.Int2ToInt16Ptr(row.Status),
TimeStart: convert.Int4ToPtr(row.TimeStart), TimeStart: convert.Int4ToPtr(row.TimeStart),
TimeEnd: convert.Int4ToPtr(row.TimeEnd), TimeEnd: convert.Int4ToPtr(row.TimeEnd),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
} }
ids = append(ids, entity.ID) ids = append(ids, entity.ID)
entities = append(entities, entity) entities = append(entities, entity)
@@ -209,17 +210,17 @@ func (r *entityRepository) Create(ctx context.Context, params sqlc.CreateEntityP
} }
entity := models.EntityEntity{ entity := models.EntityEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Name: row.Name, Name: row.Name,
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Description: convert.TextToString(row.Description), Description: convert.TextToString(row.Description),
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status), Status: convert.Int2ToInt16Ptr(row.Status),
TimeStart: convert.Int4ToPtr(row.TimeStart), TimeStart: convert.Int4ToPtr(row.TimeStart),
TimeEnd: convert.Int4ToPtr(row.TimeEnd), TimeEnd: convert.Int4ToPtr(row.TimeEnd),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
} }
return &entity, nil return &entity, nil
@@ -231,19 +232,20 @@ func (r *entityRepository) Update(ctx context.Context, params sqlc.UpdateEntityP
return nil, err return nil, err
} }
entity := models.EntityEntity{ entity := models.EntityEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Name: row.Name, Name: row.Name,
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Description: convert.TextToString(row.Description), Description: convert.TextToString(row.Description),
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status), Status: convert.Int2ToInt16Ptr(row.Status),
TimeStart: convert.Int4ToPtr(row.TimeStart), TimeStart: convert.Int4ToPtr(row.TimeStart),
TimeEnd: convert.Int4ToPtr(row.TimeEnd), TimeEnd: convert.Int4ToPtr(row.TimeEnd),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
} }
_ = r.c.Del(ctx, fmt.Sprintf("entity:id:%s", entity.ID)) _ = 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 return &entity, nil
} }
@@ -274,17 +276,17 @@ func (r *entityRepository) GetByProjectID(ctx context.Context, projectID pgtype.
for _, row := range rows { for _, row := range rows {
entity := &models.EntityEntity{ entity := &models.EntityEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Name: row.Name, Name: row.Name,
Slug: convert.TextToString(row.Slug), Slug: convert.TextToString(row.Slug),
Description: convert.TextToString(row.Description), Description: convert.TextToString(row.Description),
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status), Status: convert.Int2ToInt16Ptr(row.Status),
TimeStart: convert.Int4ToPtr(row.TimeStart), TimeStart: convert.Int4ToPtr(row.TimeStart),
TimeEnd: convert.Int4ToPtr(row.TimeEnd), TimeEnd: convert.Int4ToPtr(row.TimeEnd),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
} }
ids = append(ids, entity.ID) ids = append(ids, entity.ID)
entities = append(entities, entity) entities = append(entities, entity)
@@ -315,3 +317,35 @@ func (r *entityRepository) DeleteByIDs(ctx context.Context, ids []pgtype.UUID) e
} }
return nil 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
}

View File

@@ -19,6 +19,7 @@ import (
type WikiRepository interface { type WikiRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.WikiEntity, error) GetByID(ctx context.Context, id pgtype.UUID) (*models.WikiEntity, error)
GetByIDs(ctx context.Context, ids []string) ([]*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) Search(ctx context.Context, params sqlc.SearchWikisParams) ([]*models.WikiEntity, error)
Create(ctx context.Context, params sqlc.CreateWikiParams) (*models.WikiEntity, error) Create(ctx context.Context, params sqlc.CreateWikiParams) (*models.WikiEntity, error)
Update(ctx context.Context, params sqlc.UpdateWikiParams) (*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{ item := models.WikiEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content), Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
@@ -143,8 +145,10 @@ func (r *wikiRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.W
wiki = models.WikiEntity{ wiki = models.WikiEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content), Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
} }
@@ -172,8 +176,10 @@ func (r *wikiRepository) Search(ctx context.Context, params sqlc.SearchWikisPara
wiki := &models.WikiEntity{ wiki := &models.WikiEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content), Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
} }
@@ -201,8 +207,10 @@ func (r *wikiRepository) Create(ctx context.Context, params sqlc.CreateWikiParam
wiki := models.WikiEntity{ wiki := models.WikiEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content), Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
} }
@@ -218,12 +226,15 @@ func (r *wikiRepository) Update(ctx context.Context, params sqlc.UpdateWikiParam
wiki := models.WikiEntity{ wiki := models.WikiEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content), Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
} }
_ = r.c.Del(ctx, fmt.Sprintf("wiki:id:%s", wiki.ID)) _ = 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 return &wiki, nil
} }
@@ -272,6 +283,7 @@ func (r *wikiRepository) GetByProjectID(ctx context.Context, projectID pgtype.UU
wiki := &models.WikiEntity{ wiki := &models.WikiEntity{
ID: convert.UUIDToString(row.ID), ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title), Title: convert.TextToString(row.Title),
Slug: convert.TextToString(row.Slug),
Content: convert.TextToString(row.Content), Content: convert.TextToString(row.Content),
IsDeleted: row.IsDeleted, IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID), ProjectID: convert.UUIDToString(row.ProjectID),
@@ -318,3 +330,32 @@ func (r *wikiRepository) DeleteEntityWiki(ctx context.Context, entityID pgtype.U
WikiID: wikiID, 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
}

View File

@@ -9,5 +9,8 @@ import (
func EntityRoutes(router fiber.Router, entityController *controllers.EntityController) { func EntityRoutes(router fiber.Router, entityController *controllers.EntityController) {
entity := router.Group("/entities") entity := router.Group("/entities")
entity.Get("/", entityController.SearchEntities) entity.Get("/", entityController.SearchEntities)
entity.Get("/slug/exists", entityController.IsExistEntitySlug)
entity.Get("/slug/:slug", entityController.GetEntityBySlug)
entity.Get("/:id", entityController.GetEntityById) entity.Get("/:id", entityController.GetEntityById)
} }

View File

@@ -9,5 +9,8 @@ import (
func WikiRoutes(router fiber.Router, wikiController *controllers.WikiController) { func WikiRoutes(router fiber.Router, wikiController *controllers.WikiController) {
wiki := router.Group("/wikis") wiki := router.Group("/wikis")
wiki.Get("/", wikiController.SearchWikis) wiki.Get("/", wikiController.SearchWikis)
wiki.Get("/slug/exists", wikiController.IsExistWikiSlug)
wiki.Get("/slug/:slug", wikiController.GetWikiBySlug)
wiki.Get("/:id", wikiController.GetWikiById) wiki.Get("/:id", wikiController.GetWikiById)
} }

View File

@@ -2,6 +2,8 @@ package services
import ( import (
"context" "context"
"database/sql"
"errors"
"history-api/internal/dtos/request" "history-api/internal/dtos/request"
"history-api/internal/dtos/response" "history-api/internal/dtos/response"
"history-api/internal/gen/sqlc" "history-api/internal/gen/sqlc"
@@ -14,6 +16,8 @@ import (
type EntityService interface { type EntityService interface {
GetEntityByID(ctx context.Context, id string) (*response.EntityResponse, *fiber.Error) 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) 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 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) { func (s *entityService) SearchEntities(ctx context.Context, req *request.SearchEntityDto) ([]*response.EntityResponse, *fiber.Error) {
limit := int32(25) limit := int32(25)
if req.Limit > 0 { if req.Limit > 0 {

View File

@@ -2,7 +2,9 @@ package services
import ( import (
"context" "context"
"database/sql"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"history-api/internal/dtos/request" "history-api/internal/dtos/request"
"history-api/internal/dtos/response" "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") 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) project, err := s.projectRepo.GetByID(ctx, projectUUID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Project not found") 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) listDeleteWikis := make([]pgtype.UUID, 0)
listDeleteGeometries := make([]pgtype.UUID, 0) listDeleteGeometries := make([]pgtype.UUID, 0)
var snapshotData request.CommitSnapshot var snapshotData request.CommitSnapshot
if status == constants.StatusTypeApproved { err = json.Unmarshal(commit.SnapshotJson, &snapshotData)
err = json.Unmarshal(commit.SnapshotJson, &snapshotData) if err != nil {
if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to parse commit snapshot")
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to parse commit snapshot") }
}
if status == constants.StatusTypeApproved {
projectUUID, err := convert.StringToUUID(commit.ProjectID) projectUUID, err := convert.StringToUUID(commit.ProjectID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID")
@@ -312,7 +343,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
}) })
if err != nil { 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]) newEntities = append(newEntities, snapshotData.Entities[i])
@@ -330,7 +361,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
}) })
if err != nil { 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]) newEntities = append(newEntities, snapshotData.Entities[i])
@@ -390,7 +421,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
_, err := geometryRepo.Update(ctx, params) _, err := geometryRepo.Update(ctx, params)
if err != nil { 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]) newGeometries = append(newGeometries, snapshotData.Geometries[i])
@@ -413,7 +444,7 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
_, err := geometryRepo.Create(ctx, params) _, err := geometryRepo.Create(ctx, params)
if err != nil { 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]) newGeometries = append(newGeometries, snapshotData.Geometries[i])
@@ -453,11 +484,12 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
_, err := wikiRepo.Update(ctx, sqlc.UpdateWikiParams{ _, err := wikiRepo.Update(ctx, sqlc.UpdateWikiParams{
ID: wikiUUID, ID: wikiUUID,
Title: convert.StringToText(wiki.Title), Title: convert.StringToText(wiki.Title),
Slug: convert.PtrToText(wiki.Slug),
Content: convert.StringToText(wiki.Doc), Content: convert.StringToText(wiki.Doc),
ProjectID: projectUUID, ProjectID: projectUUID,
}) })
if err != nil { 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]) newWikis = append(newWikis, snapshotData.Wikis[i])
@@ -465,11 +497,12 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
_, err := wikiRepo.Create(ctx, sqlc.CreateWikiParams{ _, err := wikiRepo.Create(ctx, sqlc.CreateWikiParams{
ID: wikiUUID, ID: wikiUUID,
Title: convert.StringToText(wiki.Title), Title: convert.StringToText(wiki.Title),
Slug: convert.PtrToText(wiki.Slug),
Content: convert.StringToText(wiki.Doc), Content: convert.StringToText(wiki.Doc),
ProjectID: projectUUID, ProjectID: projectUUID,
}) })
if err != nil { 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]) newWikis = append(newWikis, snapshotData.Wikis[i])

View File

@@ -2,6 +2,8 @@ package services
import ( import (
"context" "context"
"database/sql"
"errors"
"history-api/internal/dtos/request" "history-api/internal/dtos/request"
"history-api/internal/dtos/response" "history-api/internal/dtos/response"
"history-api/internal/gen/sqlc" "history-api/internal/gen/sqlc"
@@ -14,6 +16,8 @@ import (
type WikiService interface { type WikiService interface {
GetWikiByID(ctx context.Context, id string) (*response.WikiResponse, *fiber.Error) 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) 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 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) { func (s *wikiService) SearchWikis(ctx context.Context, req *request.SearchWikiDto) ([]*response.WikiResponse, *fiber.Error) {
limit := int32(25) limit := int32(25)
if req.Limit > 0 { if req.Limit > 0 {

View File

@@ -17,6 +17,7 @@ var (
EMAIL_REGEX = regexp.MustCompile(`^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$`) 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})`) YOUTUBE_VIDEO_ID_REGEX = regexp.MustCompile(`(?:\/|v=|\/v\/|embed\/|watch\?v=|watch\?.+&v=)([\w-]{11})`)
BANK_INPUT = regexp.MustCompile(`[__]{2,}`) BANK_INPUT = regexp.MustCompile(`[__]{2,}`)
SLUG_REGEX = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
) )
func ValidatePassword(password string) error { func ValidatePassword(password string) error {

View File

@@ -7,6 +7,8 @@ import (
"reflect" "reflect"
"strings" "strings"
"history-api/pkg/constants"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
"github.com/google/uuid" "github.com/google/uuid"
@@ -53,6 +55,14 @@ func init() {
} }
return u.Version() == 7 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 { func isValidURL(s string) bool {