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(),
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);

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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": [

View File

@@ -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": [

View File

@@ -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.

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

View File

@@ -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 {

View File

@@ -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"`

View File

@@ -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

View File

@@ -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"`

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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])

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {