UPDATE: Submission module
All checks were successful
Build and Release / release (push) Successful in 1m14s

This commit is contained in:
2026-05-04 09:55:17 +07:00
parent f3f2e09fd5
commit bcc2e192c1
48 changed files with 2918 additions and 359 deletions

View File

@@ -105,7 +105,10 @@ func (s *FiberServer) SetupServer(
wikiService := services.NewWikiService(wikiRepo) wikiService := services.NewWikiService(wikiRepo)
projectService := services.NewProjectService(projectRepo) projectService := services.NewProjectService(projectRepo)
commitService := services.NewCommitService(poolPg, commitRepo, projectRepo) commitService := services.NewCommitService(poolPg, commitRepo, projectRepo)
submissionService := services.NewSubmissionService(submissionRepo, projectRepo, commitRepo, userRepo, poolPg, redis) submissionService := services.NewSubmissionService(
submissionRepo, projectRepo, commitRepo,
userRepo, wikiRepo, geometryRepo, entityRepo, poolPg, redis,
)
// controller setup // controller setup
authController := controllers.NewAuthController(authService, oauth) authController := controllers.NewAuthController(authService, oauth)

355
commit_snapshot.md Normal file
View File

@@ -0,0 +1,355 @@
# Commit Snapshot (`commits.snapshot_json`) - Chuẩn Hiện Tại (FrontEndAdmin)
Tài liệu này mô tả **commit snapshot** được `FrontEndAdmin` tạo ra khi bấm **Commit** trong `/editor`, và được lưu vào `BackEndGo.commits.snapshot_json` (JSONB).
Nguồn tham chiếu trong code:
- Type snapshot: `FrontEndAdmin/src/uhm/types/sections.ts` (`EditorSnapshot`)
- Build snapshot: `FrontEndAdmin/src/uhm/lib/editor/snapshot/editorSnapshot.ts` (`buildEditorSnapshot`)
## 1) Tổng Quan Schema
Snapshot hiện tại:
- Không có `schema_version`.
- Không lưu `section` (entity trong DB là `projects`; project được xác định bằng context record `commits.project_id`).
- Không dùng `ref:{id}` nữa: **`id` là canonical**, `source:"ref"` nghĩa là tham chiếu theo `id`.
```ts
export type CommitSnapshot = {
editor_feature_collection?: FeatureCollection;
entities?: EntitySnapshot[];
geometries?: GeometrySnapshot[];
wikis?: WikiSnapshot[];
geometry_entity?: GeometryEntitySnapshot[]; // geometry ↔ entity (many-to-many)
entity_wiki?: EntityWikiLinkSnapshot[]; // entity ↔ wiki
};
```
## 1.1 Type đầy đủ (TypeScript)
Đây là bản type "đúng để BEGo implement chuyển đổi snapshot → DB". FE có thể gửi thêm field legacy (xem mục 6), nhưng BE nên normalize theo các type dưới đây.
```ts
// ---- GeoJSON ----
export type Geometry =
| { type: "Point"; coordinates: [number, number] }
| { type: "MultiPoint"; coordinates: [number, number][] }
| { type: "LineString"; coordinates: [number, number][] }
| { type: "MultiLineString"; coordinates: [number, number][][] }
| { type: "Polygon"; coordinates: [number, number][][] }
| { type: "MultiPolygon"; coordinates: [number, number][][][] };
export type FeatureId = string | number; // FE hiện dùng UUIDv7 string
export type FeatureProperties = {
id: FeatureId;
type?: string | null;
geometry_preset?: string | null;
time_start?: number | null;
time_end?: number | null;
binding?: string[]; // entity ids used as "binding filter"
// Legacy UI fields. FE persist snapshot hiện tại KHONG gửi các field này,
// nhưng BE nên ignore nếu gặp trong snapshot cũ:
entity_id?: string | null;
entity_ids?: string[];
entity_name?: string | null;
entity_names?: string[];
entity_type_id?: string | null;
};
export type Feature = {
type: "Feature";
properties: FeatureProperties;
geometry: Geometry;
};
export type FeatureCollection = {
type: "FeatureCollection";
features: Feature[];
};
// ---- Snapshot rows ----
export type SnapshotSource = "inline" | "ref";
export type EntitySnapshotOperation = "create" | "update" | "delete" | "reference";
export type GeometrySnapshotOperation = "create" | "update" | "delete" | "reference";
export type WikiSnapshotOperation = "create" | "update" | "delete" | "reference";
export type EntitySnapshot = {
id: string; // UUIDv7 string (canonical)
source: SnapshotSource;
operation?: EntitySnapshotOperation;
name?: string;
slug?: string | null;
description?: string | null;
type_id?: string | null;
status?: number | null;
base_updated_at?: string;
base_hash?: string;
};
export type GeometrySnapshot = {
id: string; // UUIDv7 string (canonical)
source: SnapshotSource;
operation?: GeometrySnapshotOperation;
// Present when source:"inline" (draft features)
type?: string | null;
draw_geometry?: Geometry;
binding?: string[];
time_start?: number | null;
time_end?: number | null;
bbox?: {
min_lng: number;
min_lat: number;
max_lng: number;
max_lat: number;
} | null;
base_updated_at?: string;
base_hash?: string;
};
export type WikiSnapshot = {
id: string; // UUIDv7 string (canonical)
source: SnapshotSource;
operation?: WikiSnapshotOperation;
title: string;
doc: unknown; // tiptap JSON (inline) hoặc null (ref)
updated_at?: string;
};
// ---- Join tables ----
export type GeometryEntitySnapshot = {
geometry_id: string;
entity_id: string;
base_links_hash?: string;
};
export type EntityWikiLinkSnapshot = {
entity_id: string;
wiki_id: string;
// If missing, BE should treat as "reference" (active link) for backwards-compat.
operation?: "reference" | "delete";
};
// ---- Root ----
export type CommitSnapshot = {
editor_feature_collection?: FeatureCollection;
entities?: EntitySnapshot[];
geometries?: GeometrySnapshot[];
wikis?: WikiSnapshot[];
geometry_entity?: GeometryEntitySnapshot[];
entity_wiki?: EntityWikiLinkSnapshot[];
};
```
## 2) Quy Ước `source` và `operation`
### 2.1 `source` (bắt buộc)
`source` bắt buộc là một trong:
- `inline`: dữ liệu được embed trong snapshot_json.
- `ref`: dữ liệu là tham chiếu (theo `id`), cần fetch bên ngoài nếu muốn đầy đủ.
FE hiện tại luôn ghi `source` cho `entities[]`, `geometries[]`, `wikis[]`.
### 2.2 `operation` (tùy chọn)
`operation` là tùy chọn. Khi **không có** `operation` thì hiểu là:
- row được đưa vào snapshot như **project context** (hoặc không đổi trong commit này),
- commit này không sửa record, và cũng không cần đánh dấu là `"reference"` để làm “đầu mối nối”.
`operation` có thể xuất hiện ở:
- `entities[].operation`: `create` | `update` | `delete` | `reference`
- `geometries[].operation`: `create` | `update` | `delete` | `reference`
- `wikis[].operation`: `create` | `update` | `delete` | `reference`
`geometry_entity[]` không có `operation` (join table state).
`entity_wiki[]` dùng `operation:"reference"|"delete"` để biểu diễn link/unlink **trong snapshot** (không phải delete trong DB).
## 3) Ý Nghĩa Từng Phần
### 3.1 `editor_feature_collection`
GeoJSON `FeatureCollection` là nguồn để:
- render map trong editor,
- làm cơ sở build `geometries[]` và join table `geometry_entity[]`.
Lưu ý quan trọng:
- Snapshot persist **không lưu** các field entity denormalize trên `feature.properties`:
`entity_id/entity_ids/entity_name/entity_names/entity_type_id`.
- Quan hệ geometry ↔ entity nằm ở `geometry_entity[]`.
- Khi load commit vào editor, FE có thể rehydrate `entity_ids/entity_id` lên features từ `geometry_entity[]` để UI hoạt động, nhưng đó không phải dữ liệu persist.
### 3.2 `entities[]`
`entities[]` là danh sách entity liên quan tới project/commit. Mỗi row có `source` và có thể có/không có `operation`.
FE build `entities[]` từ:
1. Pending entities tạo mới trong editor:
`source:"inline"`, `operation:"create"`.
2. Entity được user “pin” vào project từ search (không gắn geometry, không link wiki):
`source:"ref"`, không có `operation`.
3. Entities xuất hiện trong `geometry_entity[]`:
`source:"ref"`, `operation:"reference"`.
4. Entities xuất hiện trong `entity_wiki[]`:
`source:"ref"`, `operation:"reference"`.
### 3.3 `geometries[]`
Mỗi `Feature` trong `editor_feature_collection.features[]` sinh 1 `GeometrySnapshot` row:
- `id = String(feature.properties.id)`
- `source:"inline"`
- `draw_geometry = feature.geometry`
- kèm `type`, `binding`, `time_start/time_end`, `bbox` (nếu tính được)
`operation` cho geometry:
- `create`: feature mới
- `update`: feature thay đổi
- (không có `operation`): feature không đổi (không delta trong commit)
Nếu feature bị xoá khỏi draft, FE thêm 1 delete row:
```json
{ "id": "g_1", "source": "ref", "operation": "delete" }
```
Lưu ý: geometry `operation:"delete"` **không xuất hiện trên map**, vì map render theo `editor_feature_collection.features[]`.
Gợi ý cho BE khi apply vào DB:
- Có thể coi `editor_feature_collection` là state hiện tại để render/map.
- `geometries[]` là "rows + deltas": sẽ có 1 row cho mỗi feature đang tồn tại trong draft (có/không có `operation`), và có thể có thêm các row `operation:"delete"` để xoá geometry khỏi project state.
### 3.4 `geometry_entity[]` (join table Geometry ↔ Entity)
Join table many-to-many giữa geometry và entity. Mỗi cặp geometry↔entity là một row:
```ts
{ geometry_id: string; entity_id: string }
```
### 3.5 `wikis[]`
Danh sách wiki của project tại thời điểm commit:
- Wiki tạo mới: `source:"inline"`, `operation:"create"`, `doc` là tiptap JSON.
- Wiki sửa: `source:"inline"`, `operation:"update"`, `doc` là tiptap JSON.
- Wiki không đổi: thường không có `operation`.
- Wiki add từ search (wiki đã có trong DB): `source:"ref"`, `operation:"reference"`, `doc` có thể là `null`.
### 3.6 `entity_wiki[]` (join table Entity ↔ Wiki)
```ts
export type EntityWikiLinkSnapshot = {
entity_id: string;
wiki_id: string;
operation?: "reference" | "delete";
};
```
Toggle link trong UI:
- Tick checkbox: `{ operation: "reference" }`
- Untick checkbox: `{ operation: "delete" }`
## 4) Ví Dụ JSON (rút gọn)
```json
{
"editor_feature_collection": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"id": "g_1",
"type": "city",
"time_start": 1200,
"time_end": 1300,
"binding": []
},
"geometry": { "type": "Point", "coordinates": [105.8, 21.0] }
}
]
},
"entities": [
{ "id": "e_2", "source": "ref", "name": "Pinned Entity" },
{ "id": "e_1", "source": "ref", "operation": "reference", "name": "Ha Noi", "type_id": "city", "status": 1 }
],
"geometries": [
{
"id": "g_1",
"source": "inline",
"operation": "update",
"type": "city",
"draw_geometry": { "type": "Point", "coordinates": [105.8, 21.0] },
"binding": [],
"time_start": 1200,
"time_end": 1300,
"bbox": { "min_lng": 105.8, "min_lat": 21.0, "max_lng": 105.8, "max_lat": 21.0 }
}
],
"geometry_entity": [
{ "geometry_id": "g_1", "entity_id": "e_1" }
],
"wikis": [
{
"id": "w_inline_1",
"source": "inline",
"operation": "create",
"title": "Overview",
"doc": { "type": "doc", "content": [{ "type": "paragraph" }] }
},
{
"id": "019d...wiki_from_db",
"source": "ref",
"operation": "reference",
"title": "Existing Wiki (DB)",
"doc": null
}
],
"entity_wiki": [
{ "entity_id": "e_1", "wiki_id": "w_inline_1", "operation": "reference" }
]
}
```
## 5) Notes Cho BackEnd (Normalize + Compat)
BE nên normalize trước khi convert snapshot → DB:
- Ignore toàn bộ field entity denormalize trên `feature.properties` (nếu có): `entity_id/entity_ids/entity_name/entity_names/entity_type_id`. Quan hệ geometry↔entity lấy từ `geometry_entity[]`.
- `entity_wiki[].operation`:
- `"reference"`: link active
- `"delete"`: link removed trong snapshot
- missing: treat as `"reference"` (compat)
## 6) Legacy Compatibility (nếu gặp snapshot cũ)
FE đã từng gửi các field legacy; BE có thể gặp nếu đang xử lý commit cũ:
- `entity_wikis` (plural) thay vì `entity_wiki` (singular): treat như nhau.
- `ref:{id}` trong `entities/geometries/wikis`: ignore (id canonical).
- `is_deleted` trong join table entity↔wiki: map sang `operation:"delete"` khi `is_deleted==1`, ngược lại `"reference"`.

View File

@@ -22,4 +22,7 @@ CREATE INDEX idx_projects_status_updated
ON projects (project_status, updated_at DESC); ON projects (project_status, updated_at DESC);
CREATE INDEX idx_projects_title_trgm CREATE INDEX idx_projects_title_trgm
ON projects USING GIN (title gin_trgm_ops); ON projects USING GIN (title gin_trgm_ops);

View File

@@ -1,17 +1,21 @@
CREATE TABLE IF NOT EXISTS entities ( 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,
name TEXT NOT NULL, name TEXT NOT NULL,
slug TEXT,
description TEXT, description TEXT,
thumbnail_url TEXT, status SMALLINT,
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 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);
CREATE INDEX idx_entities_project_id ON entities(project_id);
CREATE INDEX idx_entities_created_active CREATE INDEX idx_entities_created_active
ON entities(created_at DESC) ON entities(created_at DESC)
WHERE is_deleted = false; WHERE is_deleted = false;

View File

@@ -2,8 +2,9 @@ CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE TABLE IF NOT EXISTS wikis ( 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,
title TEXT, title TEXT,
content TEXT, content JSONB,
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()
@@ -13,9 +14,12 @@ CREATE TABLE IF NOT EXISTS wikis (
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,
wiki_id UUID REFERENCES wikis(id) ON DELETE CASCADE, wiki_id UUID REFERENCES wikis(id) ON DELETE CASCADE,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
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 INDEX idx_entity_wikis_wiki_id CREATE INDEX idx_entity_wikis_wiki_id
ON entity_wikis(wiki_id); ON entity_wikis(wiki_id);
@@ -27,6 +31,8 @@ CREATE INDEX idx_wikis_title_search
ON wikis USING GIN (title gin_trgm_ops) ON wikis USING GIN (title gin_trgm_ops)
WHERE is_deleted = false; WHERE is_deleted = false;
CREATE INDEX idx_wikis_project_id ON wikis(project_id);
CREATE TRIGGER trigger_wikis_updated_at CREATE TRIGGER trigger_wikis_updated_at
BEFORE UPDATE ON wikis BEFORE UPDATE ON wikis
FOR EACH ROW FOR EACH ROW

View File

@@ -4,6 +4,7 @@ CREATE EXTENSION IF NOT EXISTS postgis;
CREATE TABLE IF NOT EXISTS geometries ( CREATE TABLE IF NOT EXISTS geometries (
id UUID PRIMARY KEY DEFAULT uuidv7(), id UUID PRIMARY KEY DEFAULT uuidv7(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
geo_type SMALLINT NOT NULL DEFAULT 1, geo_type SMALLINT NOT NULL DEFAULT 1,
draw_geometry JSONB NOT NULL, draw_geometry JSONB NOT NULL,
binding JSONB, binding JSONB,
@@ -18,15 +19,12 @@ CREATE TABLE IF NOT EXISTS geometries (
CREATE TABLE IF NOT EXISTS entity_geometries ( CREATE TABLE IF NOT EXISTS entity_geometries (
entity_id UUID REFERENCES entities(id) ON DELETE CASCADE, entity_id UUID REFERENCES entities(id) ON DELETE CASCADE,
geometry_id UUID REFERENCES geometries(id) ON DELETE CASCADE, geometry_id UUID REFERENCES geometries(id) ON DELETE CASCADE,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
PRIMARY KEY (entity_id, geometry_id) PRIMARY KEY (entity_id, geometry_id)
); );
DROP INDEX IF EXISTS idx_geom_draw_geometry; CREATE INDEX idx_entity_geometries_project_id ON entity_geometries(project_id);
DROP INDEX IF EXISTS idx_geom_bbox;
DROP INDEX IF EXISTS idx_geom_time_range;
DROP INDEX IF EXISTS idx_entity_geometries_geometry;
DROP INDEX IF EXISTS idx_geom_binding;
DROP INDEX IF EXISTS idx_geom_updated_at;
CREATE INDEX idx_geom_draw_geometry CREATE INDEX idx_geom_draw_geometry
ON geometries USING GIN (draw_geometry); ON geometries USING GIN (draw_geometry);
@@ -49,6 +47,8 @@ CREATE INDEX idx_geom_updated_at
ON geometries (updated_at DESC) ON geometries (updated_at DESC)
WHERE is_deleted = false; WHERE is_deleted = false;
CREATE INDEX idx_geometries_project_id ON geometries(project_id);
DROP TRIGGER IF EXISTS trigger_geometries_updated_at ON geometries; DROP TRIGGER IF EXISTS trigger_geometries_updated_at ON geometries;
CREATE TRIGGER trigger_geometries_updated_at CREATE TRIGGER trigger_geometries_updated_at
BEFORE UPDATE ON geometries BEFORE UPDATE ON geometries

View File

@@ -34,3 +34,9 @@ LIMIT sqlc.arg('limit');
-- name: GetCommitsByIDs :many -- name: GetCommitsByIDs :many
SELECT * FROM commits WHERE id = ANY($1::uuid[]) AND is_deleted = false; SELECT * FROM commits WHERE id = ANY($1::uuid[]) AND is_deleted = false;
-- name: UpdateCommitSnapshot :one
UPDATE commits
SET snapshot_json = $2
WHERE id = $1
RETURNING *;

View File

@@ -1,8 +1,8 @@
-- name: CreateEntity :one -- name: CreateEntity :one
INSERT INTO entities ( INSERT INTO entities (
name, description, thumbnail_url id, name, slug, description, project_id, status
) VALUES ( ) VALUES (
$1, $2, $3 COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3, $4, $5
) )
RETURNING *; RETURNING *;
@@ -17,8 +17,10 @@ WHERE id = $1 AND is_deleted = false;
UPDATE entities UPDATE entities
SET SET
name = COALESCE(sqlc.narg('name'), name), name = COALESCE(sqlc.narg('name'), name),
slug = COALESCE(sqlc.narg('slug'), slug),
description = COALESCE(sqlc.narg('description'), description), description = COALESCE(sqlc.narg('description'), description),
thumbnail_url = COALESCE(sqlc.narg('thumbnail_url'), thumbnail_url) project_id = COALESCE(sqlc.narg('project_id'), project_id),
status = COALESCE(sqlc.narg('status'), status)
WHERE id = sqlc.arg('id') AND is_deleted = false WHERE id = sqlc.arg('id') AND is_deleted = false
RETURNING *; RETURNING *;
@@ -34,6 +36,7 @@ WHERE id = $1;
SELECT * SELECT *
FROM entities FROM entities
WHERE is_deleted = false WHERE is_deleted = false
AND (sqlc.narg('project_id')::uuid IS NULL OR project_id = sqlc.narg('project_id')::uuid)
AND name ILIKE '%' || sqlc.arg('name')::text || '%' AND name ILIKE '%' || sqlc.arg('name')::text || '%'
AND (sqlc.narg('cursor_id')::uuid IS NULL OR id < sqlc.narg('cursor_id')::uuid) AND (sqlc.narg('cursor_id')::uuid IS NULL OR id < sqlc.narg('cursor_id')::uuid)
ORDER BY id DESC ORDER BY id DESC
@@ -41,3 +44,13 @@ LIMIT sqlc.arg('limit_count');
-- name: GetEntitiesByIDs :many -- name: GetEntitiesByIDs :many
SELECT * FROM entities WHERE id = ANY($1::uuid[]) AND is_deleted = false; SELECT * FROM entities WHERE id = ANY($1::uuid[]) AND is_deleted = false;
-- name: GetEntitiesByProjectId :many
SELECT *
FROM entities
WHERE project_id = $1 AND is_deleted = false;
-- name: DeleteEntitiesByIDs :exec
UPDATE entities
SET is_deleted = true
WHERE id = ANY($1::uuid[]);

View File

@@ -1,15 +1,15 @@
-- name: CreateGeometry :one -- name: CreateGeometry :one
INSERT INTO geometries ( INSERT INTO geometries (
geo_type, draw_geometry, binding, time_start, time_end, bbox id, geo_type, draw_geometry, binding, time_start, time_end, bbox, project_id
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, ST_MakeEnvelope(sqlc.arg('min_lng')::float8, sqlc.arg('min_lat')::float8, sqlc.arg('max_lng')::float8, sqlc.arg('max_lat')::float8, 4326) COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3, $4, $5, ST_MakeEnvelope(sqlc.arg('min_lng')::float8, sqlc.arg('min_lat')::float8, sqlc.arg('max_lng')::float8, sqlc.arg('max_lat')::float8, 4326), $6
) )
RETURNING id, geo_type, draw_geometry, binding, time_start, time_end, RETURNING id, geo_type, draw_geometry, binding, time_start, time_end, project_id,
ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat, ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat,
is_deleted, created_at, updated_at; is_deleted, created_at, updated_at;
-- name: GetGeometryById :one -- name: GetGeometryById :one
SELECT id, geo_type, draw_geometry, binding, time_start, time_end, SELECT id, geo_type, draw_geometry, binding, time_start, time_end, project_id,
ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat, ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat,
is_deleted, created_at, updated_at is_deleted, created_at, updated_at
FROM geometries FROM geometries
@@ -23,6 +23,7 @@ SET
binding = COALESCE(sqlc.narg('binding'), binding), binding = COALESCE(sqlc.narg('binding'), binding),
time_start = COALESCE(sqlc.narg('time_start'), time_start), time_start = COALESCE(sqlc.narg('time_start'), time_start),
time_end = COALESCE(sqlc.narg('time_end'), time_end), time_end = COALESCE(sqlc.narg('time_end'), time_end),
project_id = COALESCE(sqlc.narg('project_id'), project_id),
bbox = CASE bbox = CASE
WHEN sqlc.narg('update_bbox')::boolean = true THEN WHEN sqlc.narg('update_bbox')::boolean = true THEN
ST_MakeEnvelope(sqlc.narg('min_lng')::float8, sqlc.narg('min_lat')::float8, sqlc.narg('max_lng')::float8, sqlc.narg('max_lat')::float8, 4326) ST_MakeEnvelope(sqlc.narg('min_lng')::float8, sqlc.narg('min_lat')::float8, sqlc.narg('max_lng')::float8, sqlc.narg('max_lat')::float8, 4326)
@@ -30,7 +31,7 @@ SET
END, END,
updated_at = now() updated_at = now()
WHERE id = sqlc.arg('id') AND is_deleted = false WHERE id = sqlc.arg('id') AND is_deleted = false
RETURNING id, geo_type, draw_geometry, binding, time_start, time_end, RETURNING id, geo_type, draw_geometry, binding, time_start, time_end, project_id,
ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat, ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat,
is_deleted, created_at, updated_at; is_deleted, created_at, updated_at;
@@ -42,7 +43,7 @@ WHERE id = $1;
-- name: SearchGeometries :many -- name: SearchGeometries :many
SELECT SELECT
g.id, g.geo_type, g.draw_geometry, g.binding, g.time_start, g.time_end, g.id, g.geo_type, g.draw_geometry, g.binding, g.time_start, g.time_end, g.project_id,
ST_XMin(g.bbox)::float8 as min_lng, ST_XMin(g.bbox)::float8 as min_lng,
ST_YMin(g.bbox)::float8 as min_lat, ST_YMin(g.bbox)::float8 as min_lat,
ST_XMax(g.bbox)::float8 as max_lng, ST_XMax(g.bbox)::float8 as max_lng,
@@ -50,6 +51,7 @@ SELECT
g.is_deleted, g.created_at, g.updated_at g.is_deleted, g.created_at, g.updated_at
FROM geometries g FROM geometries g
WHERE g.is_deleted = false WHERE g.is_deleted = false
AND (sqlc.narg('project_id')::uuid IS NULL OR g.project_id = sqlc.narg('project_id')::uuid)
AND ( AND (
sqlc.narg('search_min_lng')::float8 IS NULL OR sqlc.narg('search_min_lng')::float8 IS NULL OR
sqlc.narg('search_min_lat')::float8 IS NULL OR sqlc.narg('search_min_lat')::float8 IS NULL OR
@@ -85,13 +87,18 @@ RETURNING geometry_id;
-- name: CreateEntityGeometries :exec -- name: CreateEntityGeometries :exec
INSERT INTO entity_geometries ( INSERT INTO entity_geometries (
entity_id, geometry_id entity_id, geometry_id, project_id
) )
SELECT $1, unnest(@geometry_ids::uuid[]); SELECT $1, unnest(@geometry_ids::uuid[]), $2
ON CONFLICT DO NOTHING;
-- name: DeleteEntityGeometriesByProjectID :exec
DELETE FROM entity_geometries
WHERE project_id = $1;
-- name: GetGeometriesByIDs :many -- name: GetGeometriesByIDs :many
SELECT SELECT
id, geo_type, draw_geometry, binding, time_start, time_end, id, geo_type, draw_geometry, binding, time_start, time_end, project_id,
ST_XMin(bbox)::float8 as min_lng, ST_XMin(bbox)::float8 as min_lng,
ST_YMin(bbox)::float8 as min_lat, ST_YMin(bbox)::float8 as min_lat,
ST_XMax(bbox)::float8 as max_lng, ST_XMax(bbox)::float8 as max_lng,
@@ -99,3 +106,27 @@ SELECT
is_deleted, created_at, updated_at is_deleted, created_at, updated_at
FROM geometries FROM geometries
WHERE id = ANY($1::uuid[]) AND is_deleted = false; WHERE id = ANY($1::uuid[]) AND is_deleted = false;
-- name: GetGeometriesByProjectId :many
SELECT
id, geo_type, draw_geometry, binding, time_start, time_end, project_id,
ST_XMin(bbox)::float8 as min_lng,
ST_YMin(bbox)::float8 as min_lat,
ST_XMax(bbox)::float8 as max_lng,
ST_YMax(bbox)::float8 as max_lat,
is_deleted, created_at, updated_at
FROM geometries
WHERE project_id = $1 AND is_deleted = false;
-- name: DeleteGeometriesByIDs :exec
UPDATE geometries
SET is_deleted = true
WHERE id = ANY($1::uuid[]);
-- name: BulkDeleteEntityGeometriesByGeometryID :exec
DELETE FROM entity_geometries
WHERE geometry_id = $1;
-- name: DeleteEntityGeometry :exec
DELETE FROM entity_geometries
WHERE entity_id = $1 AND geometry_id = $2;

View File

@@ -17,7 +17,7 @@ SELECT
'avatar_url', up.avatar_url 'avatar_url', up.avatar_url
)::json AS user, )::json AS user,
'[]'::json AS commits, '[]'::json AS commits,
'{}'::uuid[] AS submission_ids, '[]'::json AS submissions,
'[]'::json AS members '[]'::json AS members
FROM inserted_project p FROM inserted_project p
JOIN users u ON p.user_id = u.id JOIN users u ON p.user_id = u.id
@@ -32,9 +32,9 @@ SELECT
'[]' '[]'
)::json AS commits, )::json AS commits,
COALESCE( COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id), (SELECT json_agg(json_build_object('id', s.id, 'status', s.status)) FROM submissions s WHERE s.project_id = p.id),
'{}' '[]'
)::uuid[] AS submission_ids, )::json AS submissions,
json_build_object( json_build_object(
'id', u.id, 'id', u.id,
'email', u.email, 'email', u.email,
@@ -85,7 +85,10 @@ RETURNING
FROM commits c WHERE c.project_id = projects.id AND c.is_deleted = false), FROM commits c WHERE c.project_id = projects.id AND c.is_deleted = false),
'[]' '[]'
)::json AS commits, )::json AS commits,
COALESCE((SELECT array_agg(id) FROM submissions WHERE project_id = projects.id), '{}')::uuid[] AS submission_ids, COALESCE(
(SELECT json_agg(json_build_object('id', s.id, 'status', s.status)) FROM submissions s WHERE s.project_id = projects.id),
'[]'
)::json AS submissions,
COALESCE( COALESCE(
(SELECT json_agg(json_build_object( (SELECT json_agg(json_build_object(
'user_id', pm.user_id, 'role', pm.role, 'user_id', pm.user_id, 'role', pm.role,
@@ -113,9 +116,9 @@ SELECT
'[]' '[]'
)::json AS commits, )::json AS commits,
COALESCE( COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id), (SELECT json_agg(json_build_object('id', s.id, 'status', s.status)) FROM submissions s WHERE s.project_id = p.id),
'{}' '[]'
)::uuid[] AS submission_ids, )::json AS submissions,
json_build_object( json_build_object(
'id', u.id, 'id', u.id,
'email', u.email, 'email', u.email,
@@ -187,9 +190,9 @@ SELECT
'[]' '[]'
)::json AS commits, )::json AS commits,
COALESCE( COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id), (SELECT json_agg(json_build_object('id', s.id, 'status', s.status)) FROM submissions s WHERE s.project_id = p.id),
'{}' '[]'
)::uuid[] AS submission_ids, )::json AS submissions,
json_build_object( json_build_object(
'id', u.id, 'id', u.id,
'email', u.email, 'email', u.email,
@@ -225,7 +228,10 @@ SELECT
FROM commits c WHERE c.project_id = p.id AND c.is_deleted = false), FROM commits c WHERE c.project_id = p.id AND c.is_deleted = false),
'[]' '[]'
)::json AS commits, )::json AS commits,
COALESCE((SELECT array_agg(id) FROM submissions WHERE project_id = p.id), '{}')::uuid[] AS submission_ids, COALESCE(
(SELECT json_agg(json_build_object('id', s.id, 'status', s.status)) FROM submissions s WHERE s.project_id = p.id),
'[]'
)::json AS submissions,
json_build_object( json_build_object(
'id', u.id, 'id', u.id,
'email', u.email, 'email', u.email,

View File

@@ -1,8 +1,8 @@
-- name: CreateWiki :one -- name: CreateWiki :one
INSERT INTO wikis ( INSERT INTO wikis (
title, content id, title, content, project_id
) VALUES ( ) VALUES (
$1, $2 COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3
) )
RETURNING *; RETURNING *;
@@ -15,7 +15,8 @@ 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),
content = COALESCE(sqlc.narg('content'), content) content = COALESCE(sqlc.narg('content'), content),
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
RETURNING *; RETURNING *;
@@ -25,10 +26,12 @@ SET
is_deleted = true is_deleted = true
WHERE id = $1; WHERE id = $1;
-- name: SearchWikis :many -- name: SearchWikis :many
SELECT w.* SELECT w.*
FROM wikis w FROM wikis w
WHERE w.is_deleted = false WHERE w.is_deleted = false
AND (sqlc.narg('project_id')::uuid IS NULL OR w.project_id = sqlc.narg('project_id')::uuid)
AND w.title ILIKE '%' || sqlc.arg('title')::text || '%' AND w.title ILIKE '%' || sqlc.arg('title')::text || '%'
AND ( AND (
sqlc.narg('entity_id')::uuid IS NULL OR sqlc.narg('entity_id')::uuid IS NULL OR
@@ -51,10 +54,33 @@ RETURNING wiki_id;
-- name: CreateEntityWikis :exec -- name: CreateEntityWikis :exec
INSERT INTO entity_wikis ( INSERT INTO entity_wikis (
entity_id, wiki_id entity_id, wiki_id, project_id
) )
SELECT $1, unnest(@wiki_ids::uuid[]); SELECT $1, unnest(@wiki_ids::uuid[]), $2
ON CONFLICT DO NOTHING;
-- name: DeleteEntityWikisByProjectID :exec
DELETE FROM entity_wikis
WHERE project_id = $1;
-- name: GetWikisByIDs :many -- name: GetWikisByIDs :many
SELECT * FROM wikis WHERE id = ANY($1::uuid[]) AND is_deleted = false; SELECT * FROM wikis WHERE id = ANY($1::uuid[]) AND is_deleted = false;
-- name: GetWikisByProjectId :many
SELECT *
FROM wikis
WHERE project_id = $1 AND is_deleted = false;
-- name: DeleteWikisByIDs :exec
UPDATE wikis
SET is_deleted = true
WHERE id = ANY($1::uuid[]);
-- name: BulkDeleteEntityWikisByWikiID :exec
DELETE FROM entity_wikis
WHERE wiki_id = $1;
-- name: DeleteEntityWiki :exec
DELETE FROM entity_wikis
WHERE entity_id = $1 AND wiki_id = $2;

View File

@@ -70,51 +70,6 @@ CREATE TABLE IF NOT EXISTS verification_medias (
PRIMARY KEY (verification_id, media_id) PRIMARY KEY (verification_id, media_id)
); );
CREATE TABLE IF NOT EXISTS entities (
id UUID PRIMARY KEY DEFAULT uuidv7(),
name TEXT NOT NULL,
description TEXT,
thumbnail_url TEXT,
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS wikis (
id UUID PRIMARY KEY DEFAULT uuidv7(),
title TEXT,
content TEXT,
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS entity_wikis (
entity_id UUID REFERENCES entities(id) ON DELETE CASCADE,
wiki_id UUID REFERENCES wikis(id) ON DELETE CASCADE,
PRIMARY KEY (entity_id, wiki_id)
);
CREATE TABLE IF NOT EXISTS geometries (
id UUID PRIMARY KEY DEFAULT uuidv7(),
geo_type SMALLINT NOT NULL DEFAULT 1,
draw_geometry JSONB NOT NULL,
binding JSONB,
time_start INT,
time_end INT,
bbox GEOMETRY(Polygon, 4326),
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS entity_geometries (
entity_id UUID REFERENCES entities(id) ON DELETE CASCADE,
geometry_id UUID REFERENCES geometries(id) ON DELETE CASCADE,
PRIMARY KEY (entity_id, geometry_id)
);
CREATE TABLE IF NOT EXISTS projects ( CREATE TABLE IF NOT EXISTS projects (
id UUID PRIMARY KEY DEFAULT uuidv7(), id UUID PRIMARY KEY DEFAULT uuidv7(),
title TEXT NOT NULL, title TEXT NOT NULL,
@@ -128,6 +83,60 @@ CREATE TABLE IF NOT EXISTS projects (
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );
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,
description TEXT,
status SMALLINT,
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
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,
content JSONB,
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS entity_wikis (
entity_id UUID REFERENCES entities(id) ON DELETE CASCADE,
wiki_id UUID REFERENCES wikis(id) ON DELETE CASCADE,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
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,
draw_geometry JSONB NOT NULL,
binding JSONB,
time_start INT,
time_end INT,
bbox GEOMETRY(Polygon, 4326),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE IF NOT EXISTS entity_geometries (
entity_id UUID REFERENCES entities(id) ON DELETE CASCADE,
geometry_id UUID REFERENCES geometries(id) ON DELETE CASCADE,
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
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

@@ -430,6 +430,11 @@ const docTemplate = `{
"type": "string", "type": "string",
"name": "name", "name": "name",
"in": "query" "in": "query"
},
{
"type": "string",
"name": "project_id",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -537,6 +542,11 @@ const docTemplate = `{
"in": "query", "in": "query",
"required": true "required": true
}, },
{
"type": "string",
"name": "project_id",
"in": "query"
},
{ {
"type": "integer", "type": "integer",
"name": "time", "name": "time",
@@ -3418,6 +3428,11 @@ const docTemplate = `{
"name": "limit", "name": "limit",
"in": "query" "in": "query"
}, },
{
"type": "string",
"name": "project_id",
"in": "query"
},
{ {
"maxLength": 1000, "maxLength": 1000,
"type": "string", "type": "string",
@@ -3500,6 +3515,29 @@ const docTemplate = `{
} }
} }
}, },
"history-api_internal_dtos_request.BBox": {
"type": "object",
"required": [
"max_lat",
"max_lng",
"min_lat",
"min_lng"
],
"properties": {
"max_lat": {
"type": "number"
},
"max_lng": {
"type": "number"
},
"min_lat": {
"type": "number"
},
"min_lng": {
"type": "number"
}
}
},
"history-api_internal_dtos_request.ChangeOwnerDto": { "history-api_internal_dtos_request.ChangeOwnerDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -3544,6 +3582,50 @@ const docTemplate = `{
} }
} }
}, },
"history-api_internal_dtos_request.CommitSnapshot": {
"type": "object",
"properties": {
"editor_feature_collection": {
"$ref": "#/definitions/history-api_internal_dtos_request.FeatureCollection"
},
"entities": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.EntitySnapshot"
}
},
"entity_wiki": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot"
}
},
"entity_wikis": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot"
}
},
"geometries": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.GeometrySnapshot"
}
},
"geometry_entity": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.GeometryEntitySnapshot"
}
},
"wikis": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.WikiSnapshot"
}
}
}
},
"history-api_internal_dtos_request.CreateCommitDto": { "history-api_internal_dtos_request.CreateCommitDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -3556,10 +3638,7 @@ const docTemplate = `{
"maxLength": 500 "maxLength": 500
}, },
"snapshot_json": { "snapshot_json": {
"type": "array", "$ref": "#/definitions/history-api_internal_dtos_request.CommitSnapshot"
"items": {
"type": "integer"
}
} }
} }
}, },
@@ -3688,6 +3767,176 @@ const docTemplate = `{
} }
} }
}, },
"history-api_internal_dtos_request.EntitySnapshot": {
"type": "object",
"required": [
"id"
],
"properties": {
"base_hash": {
"type": "string"
},
"base_updated_at": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"operation": {
"type": "string",
"enum": [
"create",
"update",
"delete",
"reference"
]
},
"slug": {
"type": "string"
},
"source": {
"type": "string",
"enum": [
"inline",
"ref"
]
},
"status": {
"type": "integer",
"enum": [
0,
1
]
},
"type_id": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.EntityWikiLinkSnapshot": {
"type": "object",
"required": [
"entity_id",
"wiki_id"
],
"properties": {
"entity_id": {
"type": "string"
},
"is_deleted": {
"description": "Legacy / Compatibility",
"type": "integer",
"enum": [
0,
1
]
},
"operation": {
"type": "string",
"enum": [
"reference",
"delete"
]
},
"wiki_id": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.Feature": {
"type": "object",
"required": [
"geometry",
"properties",
"type"
],
"properties": {
"geometry": {
"type": "array",
"items": {
"type": "integer"
}
},
"properties": {
"$ref": "#/definitions/history-api_internal_dtos_request.FeatureProperties"
},
"type": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.FeatureCollection": {
"type": "object",
"required": [
"features",
"type"
],
"properties": {
"features": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.Feature"
}
},
"type": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.FeatureProperties": {
"type": "object",
"required": [
"id"
],
"properties": {
"binding": {
"type": "array",
"items": {
"type": "string"
}
},
"entity_id": {
"type": "string"
},
"entity_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"entity_name": {
"type": "string"
},
"entity_names": {
"type": "array",
"items": {
"type": "string"
}
},
"entity_type_id": {
"type": "string"
},
"geometry_preset": {
"type": "string"
},
"id": {},
"time_end": {
"type": "number"
},
"time_start": {
"type": "number"
},
"type": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.ForgotPasswordDto": { "history-api_internal_dtos_request.ForgotPasswordDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -3711,6 +3960,82 @@ const docTemplate = `{
} }
} }
}, },
"history-api_internal_dtos_request.GeometryEntitySnapshot": {
"type": "object",
"required": [
"entity_id",
"geometry_id"
],
"properties": {
"base_links_hash": {
"type": "string"
},
"entity_id": {
"type": "string"
},
"geometry_id": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.GeometrySnapshot": {
"type": "object",
"required": [
"id",
"type"
],
"properties": {
"base_hash": {
"type": "string"
},
"base_updated_at": {
"type": "string"
},
"bbox": {
"$ref": "#/definitions/history-api_internal_dtos_request.BBox"
},
"binding": {
"type": "array",
"items": {
"type": "string"
}
},
"draw_geometry": {
"type": "array",
"items": {
"type": "integer"
}
},
"id": {
"type": "string"
},
"operation": {
"type": "string",
"enum": [
"create",
"update",
"delete",
"reference"
]
},
"source": {
"type": "string",
"enum": [
"inline",
"ref"
]
},
"time_end": {
"type": "number"
},
"time_start": {
"type": "number"
},
"type": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.MediaBulkDeleteDto": { "history-api_internal_dtos_request.MediaBulkDeleteDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -3934,6 +4259,46 @@ const docTemplate = `{
} }
} }
}, },
"history-api_internal_dtos_request.WikiSnapshot": {
"type": "object",
"required": [
"id",
"title"
],
"properties": {
"doc": {
"type": "array",
"items": {
"type": "integer"
}
},
"id": {
"type": "string"
},
"operation": {
"type": "string",
"enum": [
"create",
"update",
"delete",
"reference"
]
},
"source": {
"type": "string",
"enum": [
"inline",
"ref"
]
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"history-api_internal_dtos_response.CommonResponse": { "history-api_internal_dtos_response.CommonResponse": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -423,6 +423,11 @@
"type": "string", "type": "string",
"name": "name", "name": "name",
"in": "query" "in": "query"
},
{
"type": "string",
"name": "project_id",
"in": "query"
} }
], ],
"responses": { "responses": {
@@ -530,6 +535,11 @@
"in": "query", "in": "query",
"required": true "required": true
}, },
{
"type": "string",
"name": "project_id",
"in": "query"
},
{ {
"type": "integer", "type": "integer",
"name": "time", "name": "time",
@@ -3411,6 +3421,11 @@
"name": "limit", "name": "limit",
"in": "query" "in": "query"
}, },
{
"type": "string",
"name": "project_id",
"in": "query"
},
{ {
"maxLength": 1000, "maxLength": 1000,
"type": "string", "type": "string",
@@ -3493,6 +3508,29 @@
} }
} }
}, },
"history-api_internal_dtos_request.BBox": {
"type": "object",
"required": [
"max_lat",
"max_lng",
"min_lat",
"min_lng"
],
"properties": {
"max_lat": {
"type": "number"
},
"max_lng": {
"type": "number"
},
"min_lat": {
"type": "number"
},
"min_lng": {
"type": "number"
}
}
},
"history-api_internal_dtos_request.ChangeOwnerDto": { "history-api_internal_dtos_request.ChangeOwnerDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -3537,6 +3575,50 @@
} }
} }
}, },
"history-api_internal_dtos_request.CommitSnapshot": {
"type": "object",
"properties": {
"editor_feature_collection": {
"$ref": "#/definitions/history-api_internal_dtos_request.FeatureCollection"
},
"entities": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.EntitySnapshot"
}
},
"entity_wiki": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot"
}
},
"entity_wikis": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot"
}
},
"geometries": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.GeometrySnapshot"
}
},
"geometry_entity": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.GeometryEntitySnapshot"
}
},
"wikis": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.WikiSnapshot"
}
}
}
},
"history-api_internal_dtos_request.CreateCommitDto": { "history-api_internal_dtos_request.CreateCommitDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -3549,10 +3631,7 @@
"maxLength": 500 "maxLength": 500
}, },
"snapshot_json": { "snapshot_json": {
"type": "array", "$ref": "#/definitions/history-api_internal_dtos_request.CommitSnapshot"
"items": {
"type": "integer"
}
} }
} }
}, },
@@ -3681,6 +3760,176 @@
} }
} }
}, },
"history-api_internal_dtos_request.EntitySnapshot": {
"type": "object",
"required": [
"id"
],
"properties": {
"base_hash": {
"type": "string"
},
"base_updated_at": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"operation": {
"type": "string",
"enum": [
"create",
"update",
"delete",
"reference"
]
},
"slug": {
"type": "string"
},
"source": {
"type": "string",
"enum": [
"inline",
"ref"
]
},
"status": {
"type": "integer",
"enum": [
0,
1
]
},
"type_id": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.EntityWikiLinkSnapshot": {
"type": "object",
"required": [
"entity_id",
"wiki_id"
],
"properties": {
"entity_id": {
"type": "string"
},
"is_deleted": {
"description": "Legacy / Compatibility",
"type": "integer",
"enum": [
0,
1
]
},
"operation": {
"type": "string",
"enum": [
"reference",
"delete"
]
},
"wiki_id": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.Feature": {
"type": "object",
"required": [
"geometry",
"properties",
"type"
],
"properties": {
"geometry": {
"type": "array",
"items": {
"type": "integer"
}
},
"properties": {
"$ref": "#/definitions/history-api_internal_dtos_request.FeatureProperties"
},
"type": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.FeatureCollection": {
"type": "object",
"required": [
"features",
"type"
],
"properties": {
"features": {
"type": "array",
"items": {
"$ref": "#/definitions/history-api_internal_dtos_request.Feature"
}
},
"type": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.FeatureProperties": {
"type": "object",
"required": [
"id"
],
"properties": {
"binding": {
"type": "array",
"items": {
"type": "string"
}
},
"entity_id": {
"type": "string"
},
"entity_ids": {
"type": "array",
"items": {
"type": "string"
}
},
"entity_name": {
"type": "string"
},
"entity_names": {
"type": "array",
"items": {
"type": "string"
}
},
"entity_type_id": {
"type": "string"
},
"geometry_preset": {
"type": "string"
},
"id": {},
"time_end": {
"type": "number"
},
"time_start": {
"type": "number"
},
"type": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.ForgotPasswordDto": { "history-api_internal_dtos_request.ForgotPasswordDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -3704,6 +3953,82 @@
} }
} }
}, },
"history-api_internal_dtos_request.GeometryEntitySnapshot": {
"type": "object",
"required": [
"entity_id",
"geometry_id"
],
"properties": {
"base_links_hash": {
"type": "string"
},
"entity_id": {
"type": "string"
},
"geometry_id": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.GeometrySnapshot": {
"type": "object",
"required": [
"id",
"type"
],
"properties": {
"base_hash": {
"type": "string"
},
"base_updated_at": {
"type": "string"
},
"bbox": {
"$ref": "#/definitions/history-api_internal_dtos_request.BBox"
},
"binding": {
"type": "array",
"items": {
"type": "string"
}
},
"draw_geometry": {
"type": "array",
"items": {
"type": "integer"
}
},
"id": {
"type": "string"
},
"operation": {
"type": "string",
"enum": [
"create",
"update",
"delete",
"reference"
]
},
"source": {
"type": "string",
"enum": [
"inline",
"ref"
]
},
"time_end": {
"type": "number"
},
"time_start": {
"type": "number"
},
"type": {
"type": "string"
}
}
},
"history-api_internal_dtos_request.MediaBulkDeleteDto": { "history-api_internal_dtos_request.MediaBulkDeleteDto": {
"type": "object", "type": "object",
"required": [ "required": [
@@ -3927,6 +4252,46 @@
} }
} }
}, },
"history-api_internal_dtos_request.WikiSnapshot": {
"type": "object",
"required": [
"id",
"title"
],
"properties": {
"doc": {
"type": "array",
"items": {
"type": "integer"
}
},
"id": {
"type": "string"
},
"operation": {
"type": "string",
"enum": [
"create",
"update",
"delete",
"reference"
]
},
"source": {
"type": "string",
"enum": [
"inline",
"ref"
]
},
"title": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"history-api_internal_dtos_response.CommonResponse": { "history-api_internal_dtos_response.CommonResponse": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -13,6 +13,22 @@ definitions:
- role - role
- user_id - user_id
type: object type: object
history-api_internal_dtos_request.BBox:
properties:
max_lat:
type: number
max_lng:
type: number
min_lat:
type: number
min_lng:
type: number
required:
- max_lat
- max_lng
- min_lat
- min_lng
type: object
history-api_internal_dtos_request.ChangeOwnerDto: history-api_internal_dtos_request.ChangeOwnerDto:
properties: properties:
new_owner_id: new_owner_id:
@@ -43,15 +59,42 @@ definitions:
required: required:
- role_ids - role_ids
type: object type: object
history-api_internal_dtos_request.CommitSnapshot:
properties:
editor_feature_collection:
$ref: '#/definitions/history-api_internal_dtos_request.FeatureCollection'
entities:
items:
$ref: '#/definitions/history-api_internal_dtos_request.EntitySnapshot'
type: array
entity_wiki:
items:
$ref: '#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot'
type: array
entity_wikis:
items:
$ref: '#/definitions/history-api_internal_dtos_request.EntityWikiLinkSnapshot'
type: array
geometries:
items:
$ref: '#/definitions/history-api_internal_dtos_request.GeometrySnapshot'
type: array
geometry_entity:
items:
$ref: '#/definitions/history-api_internal_dtos_request.GeometryEntitySnapshot'
type: array
wikis:
items:
$ref: '#/definitions/history-api_internal_dtos_request.WikiSnapshot'
type: array
type: object
history-api_internal_dtos_request.CreateCommitDto: history-api_internal_dtos_request.CreateCommitDto:
properties: properties:
edit_summary: edit_summary:
maxLength: 500 maxLength: 500
type: string type: string
snapshot_json: snapshot_json:
items: $ref: '#/definitions/history-api_internal_dtos_request.CommitSnapshot'
type: integer
type: array
required: required:
- edit_summary - edit_summary
- snapshot_json - snapshot_json
@@ -143,6 +186,122 @@ definitions:
- content - content
- verify_type - verify_type
type: object type: object
history-api_internal_dtos_request.EntitySnapshot:
properties:
base_hash:
type: string
base_updated_at:
type: string
description:
type: string
id:
type: string
name:
type: string
operation:
enum:
- create
- update
- delete
- reference
type: string
slug:
type: string
source:
enum:
- inline
- ref
type: string
status:
enum:
- 0
- 1
type: integer
type_id:
type: string
required:
- id
type: object
history-api_internal_dtos_request.EntityWikiLinkSnapshot:
properties:
entity_id:
type: string
is_deleted:
description: Legacy / Compatibility
enum:
- 0
- 1
type: integer
operation:
enum:
- reference
- delete
type: string
wiki_id:
type: string
required:
- entity_id
- wiki_id
type: object
history-api_internal_dtos_request.Feature:
properties:
geometry:
items:
type: integer
type: array
properties:
$ref: '#/definitions/history-api_internal_dtos_request.FeatureProperties'
type:
type: string
required:
- geometry
- properties
- type
type: object
history-api_internal_dtos_request.FeatureCollection:
properties:
features:
items:
$ref: '#/definitions/history-api_internal_dtos_request.Feature'
type: array
type:
type: string
required:
- features
- type
type: object
history-api_internal_dtos_request.FeatureProperties:
properties:
binding:
items:
type: string
type: array
entity_id:
type: string
entity_ids:
items:
type: string
type: array
entity_name:
type: string
entity_names:
items:
type: string
type: array
entity_type_id:
type: string
geometry_preset:
type: string
id: {}
time_end:
type: number
time_start:
type: number
type:
type: string
required:
- id
type: object
history-api_internal_dtos_request.ForgotPasswordDto: history-api_internal_dtos_request.ForgotPasswordDto:
properties: properties:
email: email:
@@ -160,6 +319,58 @@ definitions:
- new_password - new_password
- token_id - token_id
type: object type: object
history-api_internal_dtos_request.GeometryEntitySnapshot:
properties:
base_links_hash:
type: string
entity_id:
type: string
geometry_id:
type: string
required:
- entity_id
- geometry_id
type: object
history-api_internal_dtos_request.GeometrySnapshot:
properties:
base_hash:
type: string
base_updated_at:
type: string
bbox:
$ref: '#/definitions/history-api_internal_dtos_request.BBox'
binding:
items:
type: string
type: array
draw_geometry:
items:
type: integer
type: array
id:
type: string
operation:
enum:
- create
- update
- delete
- reference
type: string
source:
enum:
- inline
- ref
type: string
time_end:
type: number
time_start:
type: number
type:
type: string
required:
- id
- type
type: object
history-api_internal_dtos_request.MediaBulkDeleteDto: history-api_internal_dtos_request.MediaBulkDeleteDto:
properties: properties:
media_ids: media_ids:
@@ -316,6 +527,34 @@ definitions:
- token - token
- token_type - token_type
type: object type: object
history-api_internal_dtos_request.WikiSnapshot:
properties:
doc:
items:
type: integer
type: array
id:
type: string
operation:
enum:
- create
- update
- delete
- reference
type: string
source:
enum:
- inline
- ref
type: string
title:
type: string
updated_at:
type: string
required:
- id
- title
type: object
history-api_internal_dtos_response.CommonResponse: history-api_internal_dtos_response.CommonResponse:
properties: properties:
data: {} data: {}
@@ -636,6 +875,9 @@ paths:
maxLength: 255 maxLength: 255
name: name name: name
type: string type: string
- in: query
name: project_id
type: string
produces: produces:
- application/json - application/json
responses: responses:
@@ -708,6 +950,9 @@ paths:
name: min_lng name: min_lng
required: true required: true
type: number type: number
- in: query
name: project_id
type: string
- in: query - in: query
name: time name: time
type: integer type: integer
@@ -2565,6 +2810,9 @@ paths:
minimum: 1 minimum: 1
name: limit name: limit
type: integer type: integer
- in: query
name: project_id
type: string
- in: query - in: query
maxLength: 1000 maxLength: 1000
name: title name: title

View File

@@ -1,9 +1,7 @@
package request package request
import "encoding/json"
type CreateCommitDto struct { type CreateCommitDto struct {
SnapshotJson json.RawMessage `json:"snapshot_json" validate:"required"` SnapshotJson *CommitSnapshot `json:"snapshot_json" validate:"required"`
EditSummary string `json:"edit_summary" validate:"required,max=500"` EditSummary string `json:"edit_summary" validate:"required,max=500"`
} }

View File

@@ -1,7 +1,8 @@
package request package request
type SearchEntityDto struct { type SearchEntityDto struct {
Cursor string `json:"cursor" query:"cursor" validate:"omitempty,uuid"` Cursor string `json:"cursor" query:"cursor" validate:"omitempty,uuid"`
Limit int `json:"limit" query:"limit" validate:"omitempty,min=1,max=100"` Limit int `json:"limit" query:"limit" validate:"omitempty,min=1,max=100"`
Name string `json:"name" query:"name" validate:"omitempty,max=255"` Name string `json:"name" query:"name" validate:"omitempty,max=255"`
ProjectID *string `json:"project_id" query:"project_id" validate:"omitempty,uuid"`
} }

View File

@@ -7,4 +7,5 @@ type SearchGeometryDto struct {
MaxLat *float64 `json:"max_lat" query:"max_lat" validate:"required,gte=-90,lte=90"` MaxLat *float64 `json:"max_lat" query:"max_lat" validate:"required,gte=-90,lte=90"`
TimePoint *int32 `json:"time" query:"time" validate:"omitempty,number"` TimePoint *int32 `json:"time" query:"time" validate:"omitempty,number"`
EntityID *string `json:"entity_id" query:"entity_id" validate:"omitempty,uuid"` EntityID *string `json:"entity_id" query:"entity_id" validate:"omitempty,uuid"`
ProjectID *string `json:"project_id" query:"project_id" validate:"omitempty,uuid"`
} }

View File

@@ -3,73 +3,63 @@ package request
import "encoding/json" import "encoding/json"
type CommitSnapshot struct { type CommitSnapshot struct {
SchemaVersion int `json:"schema_version" validate:"required"` EditorFeatureCollection *FeatureCollection `json:"editor_feature_collection,omitempty" validate:"omitempty"`
Section SectionRef `json:"section" validate:"required"` Entities []*EntitySnapshot `json:"entities,omitempty" validate:"omitempty,dive"`
EditorFeatureCollection *FeatureCollection `json:"editor_feature_collection,omitempty" validate:"omitempty"` Geometries []*GeometrySnapshot `json:"geometries,omitempty" validate:"omitempty,dive"`
Entities []EntitySnapshot `json:"entities,omitempty" validate:"omitempty,dive"` Wikis []*WikiSnapshot `json:"wikis,omitempty" validate:"omitempty,dive"`
Geometries []GeometrySnapshot `json:"geometries,omitempty" validate:"omitempty,dive"` GeometryEntity []*GeometryEntitySnapshot `json:"geometry_entity,omitempty" validate:"omitempty,dive"`
LinkScopes []LinkScopeSnapshot `json:"link_scopes,omitempty" validate:"omitempty,dive"` EntityWiki []*EntityWikiLinkSnapshot `json:"entity_wiki,omitempty" validate:"omitempty,dive"`
Wikis []WikiSnapshot `json:"wikis,omitempty" validate:"omitempty,dive"` EntityWikis []*EntityWikiLinkSnapshot `json:"entity_wikis,omitempty" validate:"omitempty,dive"`
EntityWikis []EntityWikiLinkSnapshot `json:"entity_wikis,omitempty" validate:"omitempty,dive"`
}
type SectionRef struct {
ID string `json:"id" validate:"required"`
Title string `json:"title" validate:"required"`
} }
type FeatureCollection struct { type FeatureCollection struct {
Type string `json:"type" validate:"required,eq=FeatureCollection"` Type string `json:"type" validate:"required,eq=FeatureCollection"`
Features []Feature `json:"features" validate:"required,dive"` Features []*Feature `json:"features" validate:"required,dive"`
} }
type Feature struct { type Feature struct {
Type string `json:"type" validate:"required,eq=Feature"` Type string `json:"type" validate:"required,eq=Feature"`
Properties FeatureProperties `json:"properties" validate:"required"` Properties *FeatureProperties `json:"properties" validate:"required"`
Geometry json.RawMessage `json:"geometry" validate:"required"` Geometry json.RawMessage `json:"geometry" validate:"required"`
} }
type FeatureProperties struct { type FeatureProperties struct {
ID any `json:"id" validate:"required"` ID any `json:"id" validate:"required"`
Type string `json:"type,omitempty"` Type string `json:"type,omitempty"`
TimeStart *float64 `json:"time_start,omitempty"` GeometryPreset string `json:"geometry_preset,omitempty"`
TimeEnd *float64 `json:"time_end,omitempty"` TimeStart *float64 `json:"time_start,omitempty"`
Binding []string `json:"binding,omitempty"` TimeEnd *float64 `json:"time_end,omitempty"`
EntityID string `json:"entity_id,omitempty"` Binding []string `json:"binding,omitempty"`
EntityIDs []string `json:"entity_ids,omitempty"` EntityID string `json:"entity_id,omitempty" validate:"omitempty,uuidv7"`
EntityName string `json:"entity_name,omitempty"` EntityIDs []string `json:"entity_ids,omitempty" validate:"omitempty,dive,uuidv7"`
EntityNames []string `json:"entity_names,omitempty"` EntityName string `json:"entity_name,omitempty"`
EntityTypeID string `json:"entity_type_id,omitempty"` EntityNames []string `json:"entity_names,omitempty"`
EntityTypeID string `json:"entity_type_id,omitempty" validate:"omitempty,uuidv7"`
} }
type EntitySnapshot struct { type EntitySnapshot struct {
ID string `json:"id" validate:"required"` 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"`
Ref *Ref `json:"ref,omitempty" validate:"omitempty"` 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,omitempty"` Slug *string `json:"slug,omitempty"`
Slug string `json:"slug,omitempty"` Description string `json:"description,omitempty"`
Description string `json:"description,omitempty"` TypeID string `json:"type_id,omitempty"`
TypeID string `json:"type_id,omitempty"` Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1"`
Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1"` BaseUpdatedAt string `json:"base_updated_at,omitempty"`
IsDeleted *int `json:"is_deleted,omitempty" validate:"omitempty,oneof=0 1"` BaseHash string `json:"base_hash,omitempty"`
BaseUpdatedAt string `json:"base_updated_at,omitempty"`
BaseHash string `json:"base_hash,omitempty"`
} }
type GeometrySnapshot struct { type GeometrySnapshot struct {
ID string `json:"id" validate:"required"` 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"`
Ref *Ref `json:"ref,omitempty" validate:"omitempty"`
Operation string `json:"operation,omitempty" validate:"omitempty,oneof=create update delete reference"` Operation string `json:"operation,omitempty" validate:"omitempty,oneof=create update delete reference"`
Type string `json:"type,omitempty"` Type string `json:"type" validate:"required"`
DrawGeometry json.RawMessage `json:"draw_geometry,omitempty"` DrawGeometry json.RawMessage `json:"draw_geometry,omitempty"`
Binding []string `json:"binding,omitempty"` Binding []string `json:"binding,omitempty"`
TimeStart *float64 `json:"time_start,omitempty"` TimeStart *float64 `json:"time_start,omitempty"`
TimeEnd *float64 `json:"time_end,omitempty"` TimeEnd *float64 `json:"time_end,omitempty"`
BBox *BBox `json:"bbox,omitempty" validate:"omitempty"` BBox *BBox `json:"bbox,omitempty" validate:"omitempty"`
IsDeleted *int `json:"is_deleted,omitempty" validate:"omitempty,oneof=0 1"`
BaseUpdatedAt string `json:"base_updated_at,omitempty"` BaseUpdatedAt string `json:"base_updated_at,omitempty"`
BaseHash string `json:"base_hash,omitempty"` BaseHash string `json:"base_hash,omitempty"`
} }
@@ -81,31 +71,26 @@ type BBox struct {
MaxLat float64 `json:"max_lat" validate:"required"` MaxLat float64 `json:"max_lat" validate:"required"`
} }
type LinkScopeSnapshot struct { type GeometryEntitySnapshot struct {
GeometryID string `json:"geometry_id" validate:"required"` GeometryID string `json:"geometry_id" validate:"required,uuidv7"`
Operation string `json:"operation" validate:"required,eq=reference"` // Theo doc chỉ có "reference" EntityID string `json:"entity_id" validate:"required,uuidv7"`
EntityIDs []string `json:"entity_ids" validate:"required,min=1"` // Đã link thì phải có ít nhất 1 entity BaseLinksHash string `json:"base_links_hash,omitempty"`
BaseLinksHash string `json:"base_links_hash,omitempty"`
} }
type WikiSnapshot struct { type WikiSnapshot struct {
ID string `json:"id" validate:"required"` 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"`
Ref *Ref `json:"ref,omitempty" validate:"omitempty"`
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 json.RawMessage `json:"doc,omitempty"` Doc json.RawMessage `json:"doc,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"` UpdatedAt string `json:"updated_at,omitempty"`
IsDeleted *int `json:"is_deleted,omitempty" validate:"omitempty,oneof=0 1"`
} }
type EntityWikiLinkSnapshot struct { type EntityWikiLinkSnapshot struct {
EntityID string `json:"entity_id" validate:"required"` EntityID string `json:"entity_id" validate:"required,uuidv7"`
WikiID string `json:"wiki_id" validate:"required"` WikiID string `json:"wiki_id" validate:"required,uuidv7"`
Operation string `json:"operation,omitempty" validate:"omitempty,oneof=reference delete"` Operation string `json:"operation,omitempty" validate:"omitempty,oneof=reference delete"`
IsDeleted *int `json:"is_deleted,omitempty" validate:"omitempty,oneof=0 1"`
}
type Ref struct { // Legacy / Compatibility
ID string `json:"id" validate:"required"` IsDeleted *int `json:"is_deleted,omitempty" validate:"omitempty,oneof=0 1"`
} }

View File

@@ -1,8 +1,9 @@
package request package request
type SearchWikiDto struct { type SearchWikiDto struct {
Cursor string `json:"cursor" query:"cursor" validate:"omitempty,uuid"` Cursor string `json:"cursor" query:"cursor" validate:"omitempty,uuid"`
Limit int `json:"limit" query:"limit" validate:"omitempty,min=1,max=100"` ProjectID *string `json:"project_id" query:"project_id" validate:"omitempty,uuid"`
Title string `json:"title" query:"title" validate:"omitempty,max=1000"` Limit int `json:"limit" query:"limit" validate:"omitempty,min=1,max=100"`
EntityID string `json:"entity_id" query:"entity_id" validate:"omitempty,uuid"` Title string `json:"title" query:"title" validate:"omitempty,max=1000"`
EntityID string `json:"entity_id" query:"entity_id" validate:"omitempty,uuid"`
} }

View File

@@ -5,9 +5,11 @@ import "time"
type EntityResponse struct { type EntityResponse struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Slug string `json:"slug,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
ThumbnailUrl string `json:"thumbnail_url,omitempty"` ProjectID string `json:"project_id"`
IsDeleted bool `json:"is_deleted"` Status *int16 `json:"status,omitempty"`
CreatedAt *time.Time `json:"created_at"` IsDeleted bool `json:"is_deleted,omitempty"`
UpdatedAt *time.Time `json:"updated_at"` CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
} }

View File

@@ -20,6 +20,7 @@ type GeometryResponse struct {
TimeStart int32 `json:"time_start,omitempty"` TimeStart int32 `json:"time_start,omitempty"`
TimeEnd int32 `json:"time_end,omitempty"` TimeEnd int32 `json:"time_end,omitempty"`
Bbox *Bbox `json:"bbox,omitempty"` Bbox *Bbox `json:"bbox,omitempty"`
ProjectID string `json:"project_id"`
IsDeleted bool `json:"is_deleted,omitempty"` IsDeleted bool `json:"is_deleted,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"` CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"`

View File

@@ -14,19 +14,24 @@ type MemberSimpleResponse struct {
AvatarUrl string `json:"avatar_url"` AvatarUrl string `json:"avatar_url"`
} }
type ProjectResponse struct { type SubmissionSimpleResponse struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` Status string `json:"status"`
Description string `json:"description"` }
LatestCommitID *string `json:"latest_commit_id,omitempty"`
ProjectStatus string `json:"project_status"` type ProjectResponse struct {
LockedBy *string `json:"locked_by,omitempty"` ID string `json:"id"`
IsDeleted bool `json:"is_deleted"` Title string `json:"title"`
UserID string `json:"user_id"` Description string `json:"description"`
CreatedAt *time.Time `json:"created_at"` LatestCommitID *string `json:"latest_commit_id,omitempty"`
UpdatedAt *time.Time `json:"updated_at"` ProjectStatus string `json:"project_status"`
User *UserSimpleResponse `json:"user,omitempty"` LockedBy *string `json:"locked_by,omitempty"`
Commits []CommitSimpleResponse `json:"commits"` IsDeleted bool `json:"is_deleted"`
SubmissionIds []string `json:"submission_ids"` UserID string `json:"user_id"`
Members []MemberSimpleResponse `json:"members"` CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
User *UserSimpleResponse `json:"user,omitempty"`
Commits []CommitSimpleResponse `json:"commits"`
Submissions []SubmissionSimpleResponse `json:"submissions"`
Members []MemberSimpleResponse `json:"members"`
} }

View File

@@ -1,12 +1,16 @@
package response package response
import "time" import (
"encoding/json"
"time"
)
type WikiResponse struct { type WikiResponse struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Content string `json:"content,omitempty"` Content json.RawMessage `json:"content,omitempty"`
IsDeleted bool `json:"is_deleted,omitempty"` ProjectID string `json:"project_id"`
CreatedAt *time.Time `json:"created_at,omitempty"` IsDeleted bool `json:"is_deleted,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
} }

View File

@@ -204,3 +204,31 @@ func (q *Queries) SearchCommits(ctx context.Context, arg SearchCommitsParams) ([
} }
return items, nil return items, nil
} }
const updateCommitSnapshot = `-- name: UpdateCommitSnapshot :one
UPDATE commits
SET snapshot_json = $2
WHERE id = $1
RETURNING id, project_id, snapshot_json, snapshot_hash, user_id, edit_summary, is_deleted, created_at
`
type UpdateCommitSnapshotParams struct {
ID pgtype.UUID `json:"id"`
SnapshotJson json.RawMessage `json:"snapshot_json"`
}
func (q *Queries) UpdateCommitSnapshot(ctx context.Context, arg UpdateCommitSnapshotParams) (Commit, error) {
row := q.db.QueryRow(ctx, updateCommitSnapshot, arg.ID, arg.SnapshotJson)
var i Commit
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.SnapshotJson,
&i.SnapshotHash,
&i.UserID,
&i.EditSummary,
&i.IsDeleted,
&i.CreatedAt,
)
return i, err
}

View File

@@ -13,27 +13,39 @@ import (
const createEntity = `-- name: CreateEntity :one const createEntity = `-- name: CreateEntity :one
INSERT INTO entities ( INSERT INTO entities (
name, description, thumbnail_url id, name, slug, description, project_id, status
) VALUES ( ) VALUES (
$1, $2, $3 COALESCE($6::uuid, uuidv7()), $1, $2, $3, $4, $5
) )
RETURNING id, name, description, thumbnail_url, is_deleted, created_at, updated_at RETURNING id, project_id, name, slug, description, status, is_deleted, created_at, updated_at
` `
type CreateEntityParams struct { type CreateEntityParams struct {
Name string `json:"name"` Name string `json:"name"`
Description pgtype.Text `json:"description"` Slug pgtype.Text `json:"slug"`
ThumbnailUrl pgtype.Text `json:"thumbnail_url"` Description pgtype.Text `json:"description"`
ProjectID pgtype.UUID `json:"project_id"`
Status pgtype.Int2 `json:"status"`
ID pgtype.UUID `json:"id"`
} }
func (q *Queries) CreateEntity(ctx context.Context, arg CreateEntityParams) (Entity, error) { func (q *Queries) CreateEntity(ctx context.Context, arg CreateEntityParams) (Entity, error) {
row := q.db.QueryRow(ctx, createEntity, arg.Name, arg.Description, arg.ThumbnailUrl) row := q.db.QueryRow(ctx, createEntity,
arg.Name,
arg.Slug,
arg.Description,
arg.ProjectID,
arg.Status,
arg.ID,
)
var i Entity var i Entity
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.ProjectID,
&i.Name, &i.Name,
&i.Slug,
&i.Description, &i.Description,
&i.ThumbnailUrl, &i.Status,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -41,6 +53,17 @@ func (q *Queries) CreateEntity(ctx context.Context, arg CreateEntityParams) (Ent
return i, err return i, err
} }
const deleteEntitiesByIDs = `-- name: DeleteEntitiesByIDs :exec
UPDATE entities
SET is_deleted = true
WHERE id = ANY($1::uuid[])
`
func (q *Queries) DeleteEntitiesByIDs(ctx context.Context, dollar_1 []pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteEntitiesByIDs, dollar_1)
return err
}
const deleteEntity = `-- name: DeleteEntity :exec const deleteEntity = `-- name: DeleteEntity :exec
UPDATE entities UPDATE entities
SET SET
@@ -54,7 +77,7 @@ func (q *Queries) DeleteEntity(ctx context.Context, id pgtype.UUID) error {
} }
const getEntitiesByIDs = `-- name: GetEntitiesByIDs :many const getEntitiesByIDs = `-- name: GetEntitiesByIDs :many
SELECT id, name, description, thumbnail_url, is_deleted, created_at, updated_at FROM entities WHERE id = ANY($1::uuid[]) AND is_deleted = false SELECT id, project_id, name, slug, description, status, is_deleted, created_at, updated_at FROM entities WHERE id = ANY($1::uuid[]) AND is_deleted = false
` `
func (q *Queries) GetEntitiesByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Entity, error) { func (q *Queries) GetEntitiesByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]Entity, error) {
@@ -68,9 +91,47 @@ func (q *Queries) GetEntitiesByIDs(ctx context.Context, dollar_1 []pgtype.UUID)
var i Entity var i Entity
if err := rows.Scan( if err := rows.Scan(
&i.ID, &i.ID,
&i.ProjectID,
&i.Name, &i.Name,
&i.Slug,
&i.Description, &i.Description,
&i.ThumbnailUrl, &i.Status,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getEntitiesByProjectId = `-- name: GetEntitiesByProjectId :many
SELECT id, project_id, name, slug, description, status, is_deleted, created_at, updated_at
FROM entities
WHERE project_id = $1 AND is_deleted = false
`
func (q *Queries) GetEntitiesByProjectId(ctx context.Context, projectID pgtype.UUID) ([]Entity, error) {
rows, err := q.db.Query(ctx, getEntitiesByProjectId, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Entity{}
for rows.Next() {
var i Entity
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Slug,
&i.Description,
&i.Status,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -86,7 +147,7 @@ func (q *Queries) GetEntitiesByIDs(ctx context.Context, dollar_1 []pgtype.UUID)
} }
const getEntityById = `-- name: GetEntityById :one const getEntityById = `-- name: GetEntityById :one
SELECT id, name, description, thumbnail_url, is_deleted, created_at, updated_at SELECT id, project_id, name, slug, description, status, is_deleted, created_at, updated_at
FROM entities FROM entities
WHERE id = $1 AND is_deleted = false WHERE id = $1 AND is_deleted = false
` `
@@ -96,9 +157,11 @@ func (q *Queries) GetEntityById(ctx context.Context, id pgtype.UUID) (Entity, er
var i Entity var i Entity
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.ProjectID,
&i.Name, &i.Name,
&i.Slug,
&i.Description, &i.Description,
&i.ThumbnailUrl, &i.Status,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -107,23 +170,30 @@ func (q *Queries) GetEntityById(ctx context.Context, id pgtype.UUID) (Entity, er
} }
const searchEntities = `-- name: SearchEntities :many const searchEntities = `-- name: SearchEntities :many
SELECT id, name, description, thumbnail_url, is_deleted, created_at, updated_at SELECT id, project_id, name, slug, description, status, is_deleted, created_at, updated_at
FROM entities FROM entities
WHERE is_deleted = false WHERE is_deleted = false
AND name ILIKE '%' || $1::text || '%' AND ($1::uuid IS NULL OR project_id = $1::uuid)
AND ($2::uuid IS NULL OR id < $2::uuid) AND name ILIKE '%' || $2::text || '%'
AND ($3::uuid IS NULL OR id < $3::uuid)
ORDER BY id DESC ORDER BY id DESC
LIMIT $3 LIMIT $4
` `
type SearchEntitiesParams struct { type SearchEntitiesParams struct {
ProjectID pgtype.UUID `json:"project_id"`
Name string `json:"name"` Name string `json:"name"`
CursorID pgtype.UUID `json:"cursor_id"` CursorID pgtype.UUID `json:"cursor_id"`
LimitCount int32 `json:"limit_count"` LimitCount int32 `json:"limit_count"`
} }
func (q *Queries) SearchEntities(ctx context.Context, arg SearchEntitiesParams) ([]Entity, error) { func (q *Queries) SearchEntities(ctx context.Context, arg SearchEntitiesParams) ([]Entity, error) {
rows, err := q.db.Query(ctx, searchEntities, arg.Name, arg.CursorID, arg.LimitCount) rows, err := q.db.Query(ctx, searchEntities,
arg.ProjectID,
arg.Name,
arg.CursorID,
arg.LimitCount,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -133,9 +203,11 @@ func (q *Queries) SearchEntities(ctx context.Context, arg SearchEntitiesParams)
var i Entity var i Entity
if err := rows.Scan( if err := rows.Scan(
&i.ID, &i.ID,
&i.ProjectID,
&i.Name, &i.Name,
&i.Slug,
&i.Description, &i.Description,
&i.ThumbnailUrl, &i.Status,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@@ -154,32 +226,40 @@ const updateEntity = `-- name: UpdateEntity :one
UPDATE entities UPDATE entities
SET SET
name = COALESCE($1, name), name = COALESCE($1, name),
description = COALESCE($2, description), slug = COALESCE($2, slug),
thumbnail_url = COALESCE($3, thumbnail_url) description = COALESCE($3, description),
WHERE id = $4 AND is_deleted = false project_id = COALESCE($4, project_id),
RETURNING id, name, description, thumbnail_url, is_deleted, created_at, updated_at status = COALESCE($5, status)
WHERE id = $6 AND is_deleted = false
RETURNING id, project_id, name, slug, description, status, is_deleted, created_at, updated_at
` `
type UpdateEntityParams struct { type UpdateEntityParams struct {
Name pgtype.Text `json:"name"` Name pgtype.Text `json:"name"`
Description pgtype.Text `json:"description"` Slug pgtype.Text `json:"slug"`
ThumbnailUrl pgtype.Text `json:"thumbnail_url"` Description pgtype.Text `json:"description"`
ID pgtype.UUID `json:"id"` ProjectID pgtype.UUID `json:"project_id"`
Status pgtype.Int2 `json:"status"`
ID pgtype.UUID `json:"id"`
} }
func (q *Queries) UpdateEntity(ctx context.Context, arg UpdateEntityParams) (Entity, error) { func (q *Queries) UpdateEntity(ctx context.Context, arg UpdateEntityParams) (Entity, error) {
row := q.db.QueryRow(ctx, updateEntity, row := q.db.QueryRow(ctx, updateEntity,
arg.Name, arg.Name,
arg.Slug,
arg.Description, arg.Description,
arg.ThumbnailUrl, arg.ProjectID,
arg.Status,
arg.ID, arg.ID,
) )
var i Entity var i Entity
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.ProjectID,
&i.Name, &i.Name,
&i.Slug,
&i.Description, &i.Description,
&i.ThumbnailUrl, &i.Status,
&i.IsDeleted, &i.IsDeleted,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,

View File

@@ -38,30 +38,42 @@ func (q *Queries) BulkDeleteEntityGeometriesByEntityId(ctx context.Context, enti
return items, nil return items, nil
} }
const bulkDeleteEntityGeometriesByGeometryID = `-- name: BulkDeleteEntityGeometriesByGeometryID :exec
DELETE FROM entity_geometries
WHERE geometry_id = $1
`
func (q *Queries) BulkDeleteEntityGeometriesByGeometryID(ctx context.Context, geometryID pgtype.UUID) error {
_, err := q.db.Exec(ctx, bulkDeleteEntityGeometriesByGeometryID, geometryID)
return err
}
const createEntityGeometries = `-- name: CreateEntityGeometries :exec const createEntityGeometries = `-- name: CreateEntityGeometries :exec
INSERT INTO entity_geometries ( INSERT INTO entity_geometries (
entity_id, geometry_id entity_id, geometry_id, project_id
) )
SELECT $1, unnest($2::uuid[]) SELECT $1, unnest($3::uuid[]), $2
ON CONFLICT DO NOTHING
` `
type CreateEntityGeometriesParams struct { type CreateEntityGeometriesParams struct {
EntityID pgtype.UUID `json:"entity_id"` EntityID pgtype.UUID `json:"entity_id"`
ProjectID pgtype.UUID `json:"project_id"`
GeometryIds []pgtype.UUID `json:"geometry_ids"` GeometryIds []pgtype.UUID `json:"geometry_ids"`
} }
func (q *Queries) CreateEntityGeometries(ctx context.Context, arg CreateEntityGeometriesParams) error { func (q *Queries) CreateEntityGeometries(ctx context.Context, arg CreateEntityGeometriesParams) error {
_, err := q.db.Exec(ctx, createEntityGeometries, arg.EntityID, arg.GeometryIds) _, err := q.db.Exec(ctx, createEntityGeometries, arg.EntityID, arg.ProjectID, arg.GeometryIds)
return err return err
} }
const createGeometry = `-- name: CreateGeometry :one const createGeometry = `-- name: CreateGeometry :one
INSERT INTO geometries ( INSERT INTO geometries (
geo_type, draw_geometry, binding, time_start, time_end, bbox id, geo_type, draw_geometry, binding, time_start, time_end, bbox, project_id
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, ST_MakeEnvelope($6::float8, $7::float8, $8::float8, $9::float8, 4326) COALESCE($7::uuid, uuidv7()), $1, $2, $3, $4, $5, ST_MakeEnvelope($8::float8, $9::float8, $10::float8, $11::float8, 4326), $6
) )
RETURNING id, geo_type, draw_geometry, binding, time_start, time_end, RETURNING id, geo_type, draw_geometry, binding, time_start, time_end, project_id,
ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat, ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat,
is_deleted, created_at, updated_at is_deleted, created_at, updated_at
` `
@@ -72,6 +84,8 @@ type CreateGeometryParams struct {
Binding []byte `json:"binding"` Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"` TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"` TimeEnd pgtype.Int4 `json:"time_end"`
ProjectID pgtype.UUID `json:"project_id"`
ID pgtype.UUID `json:"id"`
MinLng float64 `json:"min_lng"` MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"` MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"` MaxLng float64 `json:"max_lng"`
@@ -85,6 +99,7 @@ type CreateGeometryRow struct {
Binding []byte `json:"binding"` Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"` TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"` TimeEnd pgtype.Int4 `json:"time_end"`
ProjectID pgtype.UUID `json:"project_id"`
MinLng float64 `json:"min_lng"` MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"` MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"` MaxLng float64 `json:"max_lng"`
@@ -101,6 +116,8 @@ func (q *Queries) CreateGeometry(ctx context.Context, arg CreateGeometryParams)
arg.Binding, arg.Binding,
arg.TimeStart, arg.TimeStart,
arg.TimeEnd, arg.TimeEnd,
arg.ProjectID,
arg.ID,
arg.MinLng, arg.MinLng,
arg.MinLat, arg.MinLat,
arg.MaxLng, arg.MaxLng,
@@ -114,6 +131,7 @@ func (q *Queries) CreateGeometry(ctx context.Context, arg CreateGeometryParams)
&i.Binding, &i.Binding,
&i.TimeStart, &i.TimeStart,
&i.TimeEnd, &i.TimeEnd,
&i.ProjectID,
&i.MinLng, &i.MinLng,
&i.MinLat, &i.MinLat,
&i.MaxLng, &i.MaxLng,
@@ -125,6 +143,42 @@ func (q *Queries) CreateGeometry(ctx context.Context, arg CreateGeometryParams)
return i, err return i, err
} }
const deleteEntityGeometriesByProjectID = `-- name: DeleteEntityGeometriesByProjectID :exec
DELETE FROM entity_geometries
WHERE project_id = $1
`
func (q *Queries) DeleteEntityGeometriesByProjectID(ctx context.Context, projectID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteEntityGeometriesByProjectID, projectID)
return err
}
const deleteEntityGeometry = `-- name: DeleteEntityGeometry :exec
DELETE FROM entity_geometries
WHERE entity_id = $1 AND geometry_id = $2
`
type DeleteEntityGeometryParams struct {
EntityID pgtype.UUID `json:"entity_id"`
GeometryID pgtype.UUID `json:"geometry_id"`
}
func (q *Queries) DeleteEntityGeometry(ctx context.Context, arg DeleteEntityGeometryParams) error {
_, err := q.db.Exec(ctx, deleteEntityGeometry, arg.EntityID, arg.GeometryID)
return err
}
const deleteGeometriesByIDs = `-- name: DeleteGeometriesByIDs :exec
UPDATE geometries
SET is_deleted = true
WHERE id = ANY($1::uuid[])
`
func (q *Queries) DeleteGeometriesByIDs(ctx context.Context, dollar_1 []pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteGeometriesByIDs, dollar_1)
return err
}
const deleteGeometry = `-- name: DeleteGeometry :exec const deleteGeometry = `-- name: DeleteGeometry :exec
UPDATE geometries UPDATE geometries
SET SET
@@ -139,7 +193,7 @@ func (q *Queries) DeleteGeometry(ctx context.Context, id pgtype.UUID) error {
const getGeometriesByIDs = `-- name: GetGeometriesByIDs :many const getGeometriesByIDs = `-- name: GetGeometriesByIDs :many
SELECT SELECT
id, geo_type, draw_geometry, binding, time_start, time_end, id, geo_type, draw_geometry, binding, time_start, time_end, project_id,
ST_XMin(bbox)::float8 as min_lng, ST_XMin(bbox)::float8 as min_lng,
ST_YMin(bbox)::float8 as min_lat, ST_YMin(bbox)::float8 as min_lat,
ST_XMax(bbox)::float8 as max_lng, ST_XMax(bbox)::float8 as max_lng,
@@ -156,6 +210,7 @@ type GetGeometriesByIDsRow struct {
Binding []byte `json:"binding"` Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"` TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"` TimeEnd pgtype.Int4 `json:"time_end"`
ProjectID pgtype.UUID `json:"project_id"`
MinLng float64 `json:"min_lng"` MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"` MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"` MaxLng float64 `json:"max_lng"`
@@ -181,6 +236,71 @@ func (q *Queries) GetGeometriesByIDs(ctx context.Context, dollar_1 []pgtype.UUID
&i.Binding, &i.Binding,
&i.TimeStart, &i.TimeStart,
&i.TimeEnd, &i.TimeEnd,
&i.ProjectID,
&i.MinLng,
&i.MinLat,
&i.MaxLng,
&i.MaxLat,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getGeometriesByProjectId = `-- name: GetGeometriesByProjectId :many
SELECT
id, geo_type, draw_geometry, binding, time_start, time_end, project_id,
ST_XMin(bbox)::float8 as min_lng,
ST_YMin(bbox)::float8 as min_lat,
ST_XMax(bbox)::float8 as max_lng,
ST_YMax(bbox)::float8 as max_lat,
is_deleted, created_at, updated_at
FROM geometries
WHERE project_id = $1 AND is_deleted = false
`
type GetGeometriesByProjectIdRow struct {
ID pgtype.UUID `json:"id"`
GeoType int16 `json:"geo_type"`
DrawGeometry json.RawMessage `json:"draw_geometry"`
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"`
ProjectID pgtype.UUID `json:"project_id"`
MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"`
MaxLat float64 `json:"max_lat"`
IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) GetGeometriesByProjectId(ctx context.Context, projectID pgtype.UUID) ([]GetGeometriesByProjectIdRow, error) {
rows, err := q.db.Query(ctx, getGeometriesByProjectId, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetGeometriesByProjectIdRow{}
for rows.Next() {
var i GetGeometriesByProjectIdRow
if err := rows.Scan(
&i.ID,
&i.GeoType,
&i.DrawGeometry,
&i.Binding,
&i.TimeStart,
&i.TimeEnd,
&i.ProjectID,
&i.MinLng, &i.MinLng,
&i.MinLat, &i.MinLat,
&i.MaxLng, &i.MaxLng,
@@ -200,7 +320,7 @@ func (q *Queries) GetGeometriesByIDs(ctx context.Context, dollar_1 []pgtype.UUID
} }
const getGeometryById = `-- name: GetGeometryById :one const getGeometryById = `-- name: GetGeometryById :one
SELECT id, geo_type, draw_geometry, binding, time_start, time_end, SELECT id, geo_type, draw_geometry, binding, time_start, time_end, project_id,
ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat, ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat,
is_deleted, created_at, updated_at is_deleted, created_at, updated_at
FROM geometries FROM geometries
@@ -214,6 +334,7 @@ type GetGeometryByIdRow struct {
Binding []byte `json:"binding"` Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"` TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"` TimeEnd pgtype.Int4 `json:"time_end"`
ProjectID pgtype.UUID `json:"project_id"`
MinLng float64 `json:"min_lng"` MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"` MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"` MaxLng float64 `json:"max_lng"`
@@ -233,6 +354,7 @@ func (q *Queries) GetGeometryById(ctx context.Context, id pgtype.UUID) (GetGeome
&i.Binding, &i.Binding,
&i.TimeStart, &i.TimeStart,
&i.TimeEnd, &i.TimeEnd,
&i.ProjectID,
&i.MinLng, &i.MinLng,
&i.MinLat, &i.MinLat,
&i.MaxLng, &i.MaxLng,
@@ -246,7 +368,7 @@ func (q *Queries) GetGeometryById(ctx context.Context, id pgtype.UUID) (GetGeome
const searchGeometries = `-- name: SearchGeometries :many const searchGeometries = `-- name: SearchGeometries :many
SELECT SELECT
g.id, g.geo_type, g.draw_geometry, g.binding, g.time_start, g.time_end, g.id, g.geo_type, g.draw_geometry, g.binding, g.time_start, g.time_end, g.project_id,
ST_XMin(g.bbox)::float8 as min_lng, ST_XMin(g.bbox)::float8 as min_lng,
ST_YMin(g.bbox)::float8 as min_lat, ST_YMin(g.bbox)::float8 as min_lat,
ST_XMax(g.bbox)::float8 as max_lng, ST_XMax(g.bbox)::float8 as max_lng,
@@ -254,36 +376,38 @@ SELECT
g.is_deleted, g.created_at, g.updated_at g.is_deleted, g.created_at, g.updated_at
FROM geometries g FROM geometries g
WHERE g.is_deleted = false WHERE g.is_deleted = false
AND ($1::uuid IS NULL OR g.project_id = $1::uuid)
AND ( AND (
$1::float8 IS NULL OR
$2::float8 IS NULL OR $2::float8 IS NULL OR
$3::float8 IS NULL OR $3::float8 IS NULL OR
$4::float8 IS NULL OR $4::float8 IS NULL OR
$5::float8 IS NULL OR
g.bbox && ST_MakeEnvelope( g.bbox && ST_MakeEnvelope(
$1::float8,
$2::float8, $2::float8,
$3::float8, $3::float8,
$4::float8, $4::float8,
$5::float8,
4326 4326
) )
) )
AND ( AND (
$5::int IS NULL OR $6::int IS NULL OR
(g.time_start <= $5::int AND g.time_end >= $5::int) (g.time_start <= $6::int AND g.time_end >= $6::int)
) )
AND ( AND (
$6::uuid IS NULL OR $7::uuid IS NULL OR
EXISTS ( EXISTS (
SELECT 1 SELECT 1
FROM entity_geometries eg FROM entity_geometries eg
WHERE eg.geometry_id = g.id WHERE eg.geometry_id = g.id
AND eg.entity_id = $6::uuid AND eg.entity_id = $7::uuid
) )
) )
ORDER BY g.id DESC ORDER BY g.id DESC
` `
type SearchGeometriesParams struct { type SearchGeometriesParams struct {
ProjectID pgtype.UUID `json:"project_id"`
SearchMinLng pgtype.Float8 `json:"search_min_lng"` SearchMinLng pgtype.Float8 `json:"search_min_lng"`
SearchMinLat pgtype.Float8 `json:"search_min_lat"` SearchMinLat pgtype.Float8 `json:"search_min_lat"`
SearchMaxLng pgtype.Float8 `json:"search_max_lng"` SearchMaxLng pgtype.Float8 `json:"search_max_lng"`
@@ -299,6 +423,7 @@ type SearchGeometriesRow struct {
Binding []byte `json:"binding"` Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"` TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"` TimeEnd pgtype.Int4 `json:"time_end"`
ProjectID pgtype.UUID `json:"project_id"`
MinLng float64 `json:"min_lng"` MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"` MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"` MaxLng float64 `json:"max_lng"`
@@ -310,6 +435,7 @@ type SearchGeometriesRow struct {
func (q *Queries) SearchGeometries(ctx context.Context, arg SearchGeometriesParams) ([]SearchGeometriesRow, error) { func (q *Queries) SearchGeometries(ctx context.Context, arg SearchGeometriesParams) ([]SearchGeometriesRow, error) {
rows, err := q.db.Query(ctx, searchGeometries, rows, err := q.db.Query(ctx, searchGeometries,
arg.ProjectID,
arg.SearchMinLng, arg.SearchMinLng,
arg.SearchMinLat, arg.SearchMinLat,
arg.SearchMaxLng, arg.SearchMaxLng,
@@ -331,6 +457,7 @@ func (q *Queries) SearchGeometries(ctx context.Context, arg SearchGeometriesPara
&i.Binding, &i.Binding,
&i.TimeStart, &i.TimeStart,
&i.TimeEnd, &i.TimeEnd,
&i.ProjectID,
&i.MinLng, &i.MinLng,
&i.MinLat, &i.MinLat,
&i.MaxLng, &i.MaxLng,
@@ -357,14 +484,15 @@ SET
binding = COALESCE($3, binding), binding = COALESCE($3, binding),
time_start = COALESCE($4, time_start), time_start = COALESCE($4, time_start),
time_end = COALESCE($5, time_end), time_end = COALESCE($5, time_end),
project_id = COALESCE($6, project_id),
bbox = CASE bbox = CASE
WHEN $6::boolean = true THEN WHEN $7::boolean = true THEN
ST_MakeEnvelope($7::float8, $8::float8, $9::float8, $10::float8, 4326) ST_MakeEnvelope($8::float8, $9::float8, $10::float8, $11::float8, 4326)
ELSE bbox ELSE bbox
END, END,
updated_at = now() updated_at = now()
WHERE id = $11 AND is_deleted = false WHERE id = $12 AND is_deleted = false
RETURNING id, geo_type, draw_geometry, binding, time_start, time_end, RETURNING id, geo_type, draw_geometry, binding, time_start, time_end, project_id,
ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat, ST_XMin(bbox)::float8 as min_lng, ST_YMin(bbox)::float8 as min_lat, ST_XMax(bbox)::float8 as max_lng, ST_YMax(bbox)::float8 as max_lat,
is_deleted, created_at, updated_at is_deleted, created_at, updated_at
` `
@@ -375,6 +503,7 @@ type UpdateGeometryParams struct {
Binding []byte `json:"binding"` Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"` TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"` TimeEnd pgtype.Int4 `json:"time_end"`
ProjectID pgtype.UUID `json:"project_id"`
UpdateBbox pgtype.Bool `json:"update_bbox"` UpdateBbox pgtype.Bool `json:"update_bbox"`
MinLng pgtype.Float8 `json:"min_lng"` MinLng pgtype.Float8 `json:"min_lng"`
MinLat pgtype.Float8 `json:"min_lat"` MinLat pgtype.Float8 `json:"min_lat"`
@@ -390,6 +519,7 @@ type UpdateGeometryRow struct {
Binding []byte `json:"binding"` Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"` TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"` TimeEnd pgtype.Int4 `json:"time_end"`
ProjectID pgtype.UUID `json:"project_id"`
MinLng float64 `json:"min_lng"` MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"` MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"` MaxLng float64 `json:"max_lng"`
@@ -406,6 +536,7 @@ func (q *Queries) UpdateGeometry(ctx context.Context, arg UpdateGeometryParams)
arg.Binding, arg.Binding,
arg.TimeStart, arg.TimeStart,
arg.TimeEnd, arg.TimeEnd,
arg.ProjectID,
arg.UpdateBbox, arg.UpdateBbox,
arg.MinLng, arg.MinLng,
arg.MinLat, arg.MinLat,
@@ -421,6 +552,7 @@ func (q *Queries) UpdateGeometry(ctx context.Context, arg UpdateGeometryParams)
&i.Binding, &i.Binding,
&i.TimeStart, &i.TimeStart,
&i.TimeEnd, &i.TimeEnd,
&i.ProjectID,
&i.MinLng, &i.MinLng,
&i.MinLat, &i.MinLat,
&i.MaxLng, &i.MaxLng,

View File

@@ -22,23 +22,27 @@ type Commit struct {
} }
type Entity struct { type Entity struct {
ID pgtype.UUID `json:"id"` ID pgtype.UUID `json:"id"`
Name string `json:"name"` ProjectID pgtype.UUID `json:"project_id"`
Description pgtype.Text `json:"description"` Name string `json:"name"`
ThumbnailUrl pgtype.Text `json:"thumbnail_url"` Slug pgtype.Text `json:"slug"`
IsDeleted bool `json:"is_deleted"` Description pgtype.Text `json:"description"`
CreatedAt pgtype.Timestamptz `json:"created_at"` Status pgtype.Int2 `json:"status"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
type EntityGeometry struct { type EntityGeometry struct {
EntityID pgtype.UUID `json:"entity_id"` EntityID pgtype.UUID `json:"entity_id"`
GeometryID pgtype.UUID `json:"geometry_id"` GeometryID pgtype.UUID `json:"geometry_id"`
ProjectID pgtype.UUID `json:"project_id"`
} }
type EntityWiki struct { type EntityWiki struct {
EntityID pgtype.UUID `json:"entity_id"` EntityID pgtype.UUID `json:"entity_id"`
WikiID pgtype.UUID `json:"wiki_id"` WikiID pgtype.UUID `json:"wiki_id"`
ProjectID pgtype.UUID `json:"project_id"`
} }
type Geometry struct { type Geometry struct {
@@ -49,6 +53,7 @@ type Geometry struct {
TimeStart pgtype.Int4 `json:"time_start"` TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"` TimeEnd pgtype.Int4 `json:"time_end"`
Bbox interface{} `json:"bbox"` Bbox interface{} `json:"bbox"`
ProjectID pgtype.UUID `json:"project_id"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
@@ -161,8 +166,9 @@ type VerificationMedia struct {
type Wiki struct { type Wiki struct {
ID pgtype.UUID `json:"id"` ID pgtype.UUID `json:"id"`
ProjectID pgtype.UUID `json:"project_id"`
Title pgtype.Text `json:"title"` Title pgtype.Text `json:"title"`
Content pgtype.Text `json:"content"` Content []byte `json:"content"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`

View File

@@ -136,7 +136,7 @@ SELECT
'avatar_url', up.avatar_url 'avatar_url', up.avatar_url
)::json AS user, )::json AS user,
'[]'::json AS commits, '[]'::json AS commits,
'{}'::uuid[] AS submission_ids, '[]'::json AS submissions,
'[]'::json AS members '[]'::json AS members
FROM inserted_project p FROM inserted_project p
JOIN users u ON p.user_id = u.id JOIN users u ON p.user_id = u.id
@@ -163,7 +163,7 @@ type CreateProjectRow struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
User []byte `json:"user"` User []byte `json:"user"`
Commits []byte `json:"commits"` Commits []byte `json:"commits"`
SubmissionIds []pgtype.UUID `json:"submission_ids"` Submissions []byte `json:"submissions"`
Members []byte `json:"members"` Members []byte `json:"members"`
} }
@@ -188,7 +188,7 @@ func (q *Queries) CreateProject(ctx context.Context, arg CreateProjectParams) (C
&i.UpdatedAt, &i.UpdatedAt,
&i.User, &i.User,
&i.Commits, &i.Commits,
&i.SubmissionIds, &i.Submissions,
&i.Members, &i.Members,
) )
return i, err return i, err
@@ -215,9 +215,9 @@ SELECT
'[]' '[]'
)::json AS commits, )::json AS commits,
COALESCE( COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id), (SELECT json_agg(json_build_object('id', s.id, 'status', s.status)) FROM submissions s WHERE s.project_id = p.id),
'{}' '[]'
)::uuid[] AS submission_ids, )::json AS submissions,
json_build_object( json_build_object(
'id', u.id, 'id', u.id,
'email', u.email, 'email', u.email,
@@ -254,7 +254,7 @@ type GetProjectByIdRow struct {
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Commits []byte `json:"commits"` Commits []byte `json:"commits"`
SubmissionIds []pgtype.UUID `json:"submission_ids"` Submissions []byte `json:"submissions"`
User []byte `json:"user"` User []byte `json:"user"`
Members []byte `json:"members"` Members []byte `json:"members"`
} }
@@ -274,7 +274,7 @@ func (q *Queries) GetProjectById(ctx context.Context, id pgtype.UUID) (GetProjec
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.Commits, &i.Commits,
&i.SubmissionIds, &i.Submissions,
&i.User, &i.User,
&i.Members, &i.Members,
) )
@@ -289,7 +289,10 @@ SELECT
FROM commits c WHERE c.project_id = p.id AND c.is_deleted = false), FROM commits c WHERE c.project_id = p.id AND c.is_deleted = false),
'[]' '[]'
)::json AS commits, )::json AS commits,
COALESCE((SELECT array_agg(id) FROM submissions WHERE project_id = p.id), '{}')::uuid[] AS submission_ids, COALESCE(
(SELECT json_agg(json_build_object('id', s.id, 'status', s.status)) FROM submissions s WHERE s.project_id = p.id),
'[]'
)::json AS submissions,
json_build_object( json_build_object(
'id', u.id, 'id', u.id,
'email', u.email, 'email', u.email,
@@ -326,7 +329,7 @@ type GetProjectsByIDsRow struct {
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Commits []byte `json:"commits"` Commits []byte `json:"commits"`
SubmissionIds []pgtype.UUID `json:"submission_ids"` Submissions []byte `json:"submissions"`
User []byte `json:"user"` User []byte `json:"user"`
Members []byte `json:"members"` Members []byte `json:"members"`
} }
@@ -352,7 +355,7 @@ func (q *Queries) GetProjectsByIDs(ctx context.Context, dollar_1 []pgtype.UUID)
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.Commits, &i.Commits,
&i.SubmissionIds, &i.Submissions,
&i.User, &i.User,
&i.Members, &i.Members,
); err != nil { ); err != nil {
@@ -375,9 +378,9 @@ SELECT
'[]' '[]'
)::json AS commits, )::json AS commits,
COALESCE( COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id), (SELECT json_agg(json_build_object('id', s.id, 'status', s.status)) FROM submissions s WHERE s.project_id = p.id),
'{}' '[]'
)::uuid[] AS submission_ids, )::json AS submissions,
json_build_object( json_build_object(
'id', u.id, 'id', u.id,
'email', u.email, 'email', u.email,
@@ -424,7 +427,7 @@ type GetProjectsByUserIdRow struct {
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Commits []byte `json:"commits"` Commits []byte `json:"commits"`
SubmissionIds []pgtype.UUID `json:"submission_ids"` Submissions []byte `json:"submissions"`
User []byte `json:"user"` User []byte `json:"user"`
Members []byte `json:"members"` Members []byte `json:"members"`
} }
@@ -450,7 +453,7 @@ func (q *Queries) GetProjectsByUserId(ctx context.Context, arg GetProjectsByUser
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.Commits, &i.Commits,
&i.SubmissionIds, &i.Submissions,
&i.User, &i.User,
&i.Members, &i.Members,
); err != nil { ); err != nil {
@@ -488,9 +491,9 @@ SELECT
'[]' '[]'
)::json AS commits, )::json AS commits,
COALESCE( COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id), (SELECT json_agg(json_build_object('id', s.id, 'status', s.status)) FROM submissions s WHERE s.project_id = p.id),
'{}' '[]'
)::uuid[] AS submission_ids, )::json AS submissions,
json_build_object( json_build_object(
'id', u.id, 'id', u.id,
'email', u.email, 'email', u.email,
@@ -561,7 +564,7 @@ type SearchProjectsRow struct {
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
Commits []byte `json:"commits"` Commits []byte `json:"commits"`
SubmissionIds []pgtype.UUID `json:"submission_ids"` Submissions []byte `json:"submissions"`
User []byte `json:"user"` User []byte `json:"user"`
Members []byte `json:"members"` Members []byte `json:"members"`
} }
@@ -597,7 +600,7 @@ func (q *Queries) SearchProjects(ctx context.Context, arg SearchProjectsParams)
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
&i.Commits, &i.Commits,
&i.SubmissionIds, &i.Submissions,
&i.User, &i.User,
&i.Members, &i.Members,
); err != nil { ); err != nil {
@@ -654,7 +657,10 @@ RETURNING
FROM commits c WHERE c.project_id = projects.id AND c.is_deleted = false), FROM commits c WHERE c.project_id = projects.id AND c.is_deleted = false),
'[]' '[]'
)::json AS commits, )::json AS commits,
COALESCE((SELECT array_agg(id) FROM submissions WHERE project_id = projects.id), '{}')::uuid[] AS submission_ids, COALESCE(
(SELECT json_agg(json_build_object('id', s.id, 'status', s.status)) FROM submissions s WHERE s.project_id = projects.id),
'[]'
)::json AS submissions,
COALESCE( COALESCE(
(SELECT json_agg(json_build_object( (SELECT json_agg(json_build_object(
'user_id', pm.user_id, 'role', pm.role, 'user_id', pm.user_id, 'role', pm.role,
@@ -690,7 +696,7 @@ type UpdateProjectRow struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
User []byte `json:"user"` User []byte `json:"user"`
Commits []byte `json:"commits"` Commits []byte `json:"commits"`
SubmissionIds []pgtype.UUID `json:"submission_ids"` Submissions []byte `json:"submissions"`
Members []byte `json:"members"` Members []byte `json:"members"`
} }
@@ -717,7 +723,7 @@ func (q *Queries) UpdateProject(ctx context.Context, arg UpdateProjectParams) (U
&i.UpdatedAt, &i.UpdatedAt,
&i.User, &i.User,
&i.Commits, &i.Commits,
&i.SubmissionIds, &i.Submissions,
&i.Members, &i.Members,
) )
return i, err return i, err

View File

@@ -37,42 +37,62 @@ func (q *Queries) BulkDeleteEntityWikisByEntityId(ctx context.Context, entityID
return items, nil return items, nil
} }
const bulkDeleteEntityWikisByWikiID = `-- name: BulkDeleteEntityWikisByWikiID :exec
DELETE FROM entity_wikis
WHERE wiki_id = $1
`
func (q *Queries) BulkDeleteEntityWikisByWikiID(ctx context.Context, wikiID pgtype.UUID) error {
_, err := q.db.Exec(ctx, bulkDeleteEntityWikisByWikiID, wikiID)
return err
}
const createEntityWikis = `-- name: CreateEntityWikis :exec const createEntityWikis = `-- name: CreateEntityWikis :exec
INSERT INTO entity_wikis ( INSERT INTO entity_wikis (
entity_id, wiki_id entity_id, wiki_id, project_id
) )
SELECT $1, unnest($2::uuid[]) SELECT $1, unnest($3::uuid[]), $2
ON CONFLICT DO NOTHING
` `
type CreateEntityWikisParams struct { type CreateEntityWikisParams struct {
EntityID pgtype.UUID `json:"entity_id"` EntityID pgtype.UUID `json:"entity_id"`
WikiIds []pgtype.UUID `json:"wiki_ids"` ProjectID pgtype.UUID `json:"project_id"`
WikiIds []pgtype.UUID `json:"wiki_ids"`
} }
func (q *Queries) CreateEntityWikis(ctx context.Context, arg CreateEntityWikisParams) error { func (q *Queries) CreateEntityWikis(ctx context.Context, arg CreateEntityWikisParams) error {
_, err := q.db.Exec(ctx, createEntityWikis, arg.EntityID, arg.WikiIds) _, err := q.db.Exec(ctx, createEntityWikis, arg.EntityID, arg.ProjectID, arg.WikiIds)
return err return err
} }
const createWiki = `-- name: CreateWiki :one const createWiki = `-- name: CreateWiki :one
INSERT INTO wikis ( INSERT INTO wikis (
title, content id, title, content, project_id
) VALUES ( ) VALUES (
$1, $2 COALESCE($4::uuid, uuidv7()), $1, $2, $3
) )
RETURNING id, title, content, is_deleted, created_at, updated_at RETURNING id, project_id, title, content, is_deleted, created_at, updated_at
` `
type CreateWikiParams struct { type CreateWikiParams struct {
Title pgtype.Text `json:"title"` Title pgtype.Text `json:"title"`
Content pgtype.Text `json:"content"` Content []byte `json:"content"`
ProjectID pgtype.UUID `json:"project_id"`
ID pgtype.UUID `json:"id"`
} }
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, arg.Title, arg.Content) row := q.db.QueryRow(ctx, createWiki,
arg.Title,
arg.Content,
arg.ProjectID,
arg.ID,
)
var i Wiki var i Wiki
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.ProjectID,
&i.Title, &i.Title,
&i.Content, &i.Content,
&i.IsDeleted, &i.IsDeleted,
@@ -82,6 +102,31 @@ func (q *Queries) CreateWiki(ctx context.Context, arg CreateWikiParams) (Wiki, e
return i, err return i, err
} }
const deleteEntityWiki = `-- name: DeleteEntityWiki :exec
DELETE FROM entity_wikis
WHERE entity_id = $1 AND wiki_id = $2
`
type DeleteEntityWikiParams struct {
EntityID pgtype.UUID `json:"entity_id"`
WikiID pgtype.UUID `json:"wiki_id"`
}
func (q *Queries) DeleteEntityWiki(ctx context.Context, arg DeleteEntityWikiParams) error {
_, err := q.db.Exec(ctx, deleteEntityWiki, arg.EntityID, arg.WikiID)
return err
}
const deleteEntityWikisByProjectID = `-- name: DeleteEntityWikisByProjectID :exec
DELETE FROM entity_wikis
WHERE project_id = $1
`
func (q *Queries) DeleteEntityWikisByProjectID(ctx context.Context, projectID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteEntityWikisByProjectID, projectID)
return err
}
const deleteWiki = `-- name: DeleteWiki :exec const deleteWiki = `-- name: DeleteWiki :exec
UPDATE wikis UPDATE wikis
SET SET
@@ -94,8 +139,19 @@ func (q *Queries) DeleteWiki(ctx context.Context, id pgtype.UUID) error {
return err return err
} }
const deleteWikisByIDs = `-- name: DeleteWikisByIDs :exec
UPDATE wikis
SET is_deleted = true
WHERE id = ANY($1::uuid[])
`
func (q *Queries) DeleteWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteWikisByIDs, dollar_1)
return err
}
const getWikiById = `-- name: GetWikiById :one const getWikiById = `-- name: GetWikiById :one
SELECT id, title, content, is_deleted, created_at, updated_at SELECT id, project_id, title, 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
` `
@@ -105,6 +161,7 @@ func (q *Queries) GetWikiById(ctx context.Context, id pgtype.UUID) (Wiki, error)
var i Wiki var i Wiki
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.ProjectID,
&i.Title, &i.Title,
&i.Content, &i.Content,
&i.IsDeleted, &i.IsDeleted,
@@ -115,7 +172,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, title, content, is_deleted, created_at, updated_at FROM wikis WHERE id = ANY($1::uuid[]) AND is_deleted = false SELECT id, project_id, title, 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) {
@@ -129,6 +186,41 @@ func (q *Queries) GetWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]
var i Wiki var i Wiki
if err := rows.Scan( if err := rows.Scan(
&i.ID, &i.ID,
&i.ProjectID,
&i.Title,
&i.Content,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWikisByProjectId = `-- name: GetWikisByProjectId :many
SELECT id, project_id, title, content, is_deleted, created_at, updated_at
FROM wikis
WHERE project_id = $1 AND is_deleted = false
`
func (q *Queries) GetWikisByProjectId(ctx context.Context, projectID pgtype.UUID) ([]Wiki, error) {
rows, err := q.db.Query(ctx, getWikisByProjectId, projectID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Wiki{}
for rows.Next() {
var i Wiki
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.Title, &i.Title,
&i.Content, &i.Content,
&i.IsDeleted, &i.IsDeleted,
@@ -146,26 +238,28 @@ func (q *Queries) GetWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]
} }
const searchWikis = `-- name: SearchWikis :many const searchWikis = `-- name: SearchWikis :many
SELECT w.id, w.title, w.content, w.is_deleted, w.created_at, w.updated_at SELECT w.id, w.project_id, w.title, 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 w.title ILIKE '%' || $1::text || '%' AND ($1::uuid IS NULL OR w.project_id = $1::uuid)
AND w.title ILIKE '%' || $2::text || '%'
AND ( AND (
$2::uuid IS NULL OR $3::uuid IS NULL OR
EXISTS ( EXISTS (
SELECT 1 SELECT 1
FROM entity_wikis ew FROM entity_wikis ew
WHERE ew.wiki_id = w.id WHERE ew.wiki_id = w.id
AND ew.entity_id = $2::uuid AND ew.entity_id = $3::uuid
) )
) )
AND ($3::uuid IS NULL OR w.id < $3::uuid) AND ($4::uuid IS NULL OR w.id < $4::uuid)
ORDER BY w.id DESC ORDER BY w.id DESC
LIMIT $4 LIMIT $5
` `
type SearchWikisParams struct { type SearchWikisParams struct {
ProjectID pgtype.UUID `json:"project_id"`
Title string `json:"title"` Title string `json:"title"`
EntityID pgtype.UUID `json:"entity_id"` EntityID pgtype.UUID `json:"entity_id"`
CursorID pgtype.UUID `json:"cursor_id"` CursorID pgtype.UUID `json:"cursor_id"`
@@ -174,6 +268,7 @@ type SearchWikisParams struct {
func (q *Queries) SearchWikis(ctx context.Context, arg SearchWikisParams) ([]Wiki, error) { func (q *Queries) SearchWikis(ctx context.Context, arg SearchWikisParams) ([]Wiki, error) {
rows, err := q.db.Query(ctx, searchWikis, rows, err := q.db.Query(ctx, searchWikis,
arg.ProjectID,
arg.Title, arg.Title,
arg.EntityID, arg.EntityID,
arg.CursorID, arg.CursorID,
@@ -188,6 +283,7 @@ func (q *Queries) SearchWikis(ctx context.Context, arg SearchWikisParams) ([]Wik
var i Wiki var i Wiki
if err := rows.Scan( if err := rows.Scan(
&i.ID, &i.ID,
&i.ProjectID,
&i.Title, &i.Title,
&i.Content, &i.Content,
&i.IsDeleted, &i.IsDeleted,
@@ -208,22 +304,30 @@ const updateWiki = `-- name: UpdateWiki :one
UPDATE wikis UPDATE wikis
SET SET
title = COALESCE($1, title), title = COALESCE($1, title),
content = COALESCE($2, content) content = COALESCE($2, content),
WHERE id = $3 AND is_deleted = false project_id = COALESCE($3, project_id)
RETURNING id, title, content, is_deleted, created_at, updated_at WHERE id = $4 AND is_deleted = false
RETURNING id, project_id, title, content, is_deleted, created_at, updated_at
` `
type UpdateWikiParams struct { type UpdateWikiParams struct {
Title pgtype.Text `json:"title"` Title pgtype.Text `json:"title"`
Content pgtype.Text `json:"content"` Content []byte `json:"content"`
ID pgtype.UUID `json:"id"` ProjectID pgtype.UUID `json:"project_id"`
ID pgtype.UUID `json:"id"`
} }
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, arg.Title, arg.Content, arg.ID) row := q.db.QueryRow(ctx, updateWiki,
arg.Title,
arg.Content,
arg.ProjectID,
arg.ID,
)
var i Wiki var i Wiki
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.ProjectID,
&i.Title, &i.Title,
&i.Content, &i.Content,
&i.IsDeleted, &i.IsDeleted,

View File

@@ -8,8 +8,10 @@ import (
type EntityEntity struct { type EntityEntity struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"` Description string `json:"description"`
ThumbnailUrl string `json:"thumbnail_url"` ProjectID string `json:"project_id"`
Status *int16 `json:"status"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"` CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"` UpdatedAt *time.Time `json:"updated_at"`
@@ -22,8 +24,10 @@ func (e *EntityEntity) ToResponse() *response.EntityResponse {
return &response.EntityResponse{ return &response.EntityResponse{
ID: e.ID, ID: e.ID,
Name: e.Name, Name: e.Name,
Slug: e.Slug,
Description: e.Description, Description: e.Description,
ThumbnailUrl: e.ThumbnailUrl, ProjectID: e.ProjectID,
Status: e.Status,
IsDeleted: e.IsDeleted, IsDeleted: e.IsDeleted,
CreatedAt: e.CreatedAt, CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt, UpdatedAt: e.UpdatedAt,

View File

@@ -15,6 +15,7 @@ type GeometryEntity struct {
TimeStart int32 `json:"time_start"` TimeStart int32 `json:"time_start"`
TimeEnd int32 `json:"time_end"` TimeEnd int32 `json:"time_end"`
Bbox *response.Bbox `json:"bbox"` Bbox *response.Bbox `json:"bbox"`
ProjectID string `json:"project_id"`
IsDeleted bool `json:"is_deleted"` IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"` CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"` UpdatedAt *time.Time `json:"updated_at"`
@@ -32,6 +33,7 @@ func (g *GeometryEntity) ToResponse() *response.GeometryResponse {
TimeStart: g.TimeStart, TimeStart: g.TimeStart,
TimeEnd: g.TimeEnd, TimeEnd: g.TimeEnd,
Bbox: g.Bbox, Bbox: g.Bbox,
ProjectID: g.ProjectID,
IsDeleted: g.IsDeleted, IsDeleted: g.IsDeleted,
CreatedAt: g.CreatedAt, CreatedAt: g.CreatedAt,
UpdatedAt: g.UpdatedAt, UpdatedAt: g.UpdatedAt,

View File

@@ -19,6 +19,11 @@ type MemberSimple struct {
AvatarUrl string `json:"avatar_url"` AvatarUrl string `json:"avatar_url"`
} }
type SubmissionSimple struct {
ID string `json:"id"`
Status constants.StatusType `json:"status"`
}
type ProjectEntity struct { type ProjectEntity struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` Title string `json:"title"`
@@ -32,7 +37,7 @@ type ProjectEntity struct {
UpdatedAt *time.Time `json:"updated_at"` UpdatedAt *time.Time `json:"updated_at"`
User *UserSimpleEntity `json:"user"` User *UserSimpleEntity `json:"user"`
Commits []CommitSimple `json:"commits"` Commits []CommitSimple `json:"commits"`
SubmissionIds []string `json:"submission_ids"` Submissions []SubmissionSimple `json:"submissions"`
Members []MemberSimple `json:"members"` Members []MemberSimple `json:"members"`
} }
@@ -60,6 +65,14 @@ func (p *ProjectEntity) ParseMembers(data []byte) error {
return json.Unmarshal(data, &p.Members) return json.Unmarshal(data, &p.Members)
} }
func (p *ProjectEntity) ParseSubmissions(data []byte) error {
if len(data) == 0 || string(data) == "null" || string(data) == "[]" {
p.Submissions = []SubmissionSimple{}
return nil
}
return json.Unmarshal(data, &p.Submissions)
}
func (p *ProjectEntity) ToResponse() *response.ProjectResponse { func (p *ProjectEntity) ToResponse() *response.ProjectResponse {
if p == nil { if p == nil {
return nil return nil
@@ -77,6 +90,14 @@ func (p *ProjectEntity) ToResponse() *response.ProjectResponse {
}) })
} }
submissions := make([]response.SubmissionSimpleResponse, 0, len(p.Submissions))
for _, s := range p.Submissions {
submissions = append(submissions, response.SubmissionSimpleResponse{
ID: s.ID,
Status: s.Status.String(),
})
}
members := make([]response.MemberSimpleResponse, 0, len(p.Members)) members := make([]response.MemberSimpleResponse, 0, len(p.Members))
for _, m := range p.Members { for _, m := range p.Members {
members = append(members, response.MemberSimpleResponse{ members = append(members, response.MemberSimpleResponse{
@@ -100,7 +121,7 @@ func (p *ProjectEntity) ToResponse() *response.ProjectResponse {
UpdatedAt: p.UpdatedAt, UpdatedAt: p.UpdatedAt,
User: userResponse, User: userResponse,
Commits: commits, Commits: commits,
SubmissionIds: p.SubmissionIds, Submissions: submissions,
Members: members, Members: members,
} }
} }

View File

@@ -1,17 +1,19 @@
package models package models
import ( import (
"encoding/json"
"history-api/internal/dtos/response" "history-api/internal/dtos/response"
"time" "time"
) )
type WikiEntity struct { type WikiEntity struct {
ID string `json:"id"` ID string `json:"id"`
Title string `json:"title"` Title string `json:"title"`
Content string `json:"content"` Content json.RawMessage `json:"content"`
IsDeleted bool `json:"is_deleted"` ProjectID string `json:"project_id"`
CreatedAt *time.Time `json:"created_at"` IsDeleted bool `json:"is_deleted"`
UpdatedAt *time.Time `json:"updated_at"` CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
} }
func (w *WikiEntity) ToResponse() *response.WikiResponse { func (w *WikiEntity) ToResponse() *response.WikiResponse {
@@ -22,6 +24,7 @@ func (w *WikiEntity) ToResponse() *response.WikiResponse {
ID: w.ID, ID: w.ID,
Title: w.Title, Title: w.Title,
Content: w.Content, Content: w.Content,
ProjectID: w.ProjectID,
IsDeleted: w.IsDeleted, IsDeleted: w.IsDeleted,
CreatedAt: w.CreatedAt, CreatedAt: w.CreatedAt,
UpdatedAt: w.UpdatedAt, UpdatedAt: w.UpdatedAt,

View File

@@ -20,6 +20,7 @@ type CommitRepository interface {
GetByID(ctx context.Context, id pgtype.UUID) (*models.CommitEntity, error) GetByID(ctx context.Context, id pgtype.UUID) (*models.CommitEntity, error)
GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.CommitEntity, error) GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.CommitEntity, error)
Search(ctx context.Context, params sqlc.SearchCommitsParams) ([]*models.CommitEntity, error) Search(ctx context.Context, params sqlc.SearchCommitsParams) ([]*models.CommitEntity, error)
UpdateSnapshot(ctx context.Context, id pgtype.UUID, snapshot json.RawMessage) (*models.CommitEntity, error)
WithTx(tx pgx.Tx) CommitRepository WithTx(tx pgx.Tx) CommitRepository
} }
@@ -228,3 +229,25 @@ func (r *commitRepository) Search(ctx context.Context, params sqlc.SearchCommits
return commits, nil return commits, nil
} }
func (r *commitRepository) UpdateSnapshot(ctx context.Context, id pgtype.UUID, snapshot json.RawMessage) (*models.CommitEntity, error) {
row, err := r.q.UpdateCommitSnapshot(ctx, sqlc.UpdateCommitSnapshotParams{
ID: id,
SnapshotJson: snapshot,
})
if err != nil {
return nil, err
}
r.c.Del(ctx, fmt.Sprintf("commit:id:%s", convert.UUIDToString(id)))
return &models.CommitEntity{
ID: convert.UUIDToString(row.ID),
ProjectID: convert.UUIDToString(row.ProjectID),
SnapshotJson: row.SnapshotJson,
SnapshotHash: convert.TextToString(row.SnapshotHash),
UserID: convert.UUIDToString(row.UserID),
EditSummary: convert.TextToString(row.EditSummary),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
}, nil
}

View File

@@ -23,6 +23,8 @@ type EntityRepository interface {
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)
Delete(ctx context.Context, id pgtype.UUID) error Delete(ctx context.Context, id pgtype.UUID) error
DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error
GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.EntityEntity, error)
WithTx(tx pgx.Tx) EntityRepository WithTx(tx pgx.Tx) EntityRepository
} }
@@ -83,8 +85,10 @@ func (r *entityRepository) getByIDsWithFallback(ctx context.Context, ids []strin
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),
Description: convert.TextToString(row.Description), Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl), ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status),
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),
@@ -136,8 +140,10 @@ 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),
Description: convert.TextToString(row.Description), Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl), ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status),
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),
@@ -166,8 +172,10 @@ func (r *entityRepository) Search(ctx context.Context, params sqlc.SearchEntitie
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),
Description: convert.TextToString(row.Description), Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl), ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status),
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),
@@ -180,6 +188,7 @@ func (r *entityRepository) Search(ctx context.Context, params sqlc.SearchEntitie
if len(entityToCache) > 0 { if len(entityToCache) > 0 {
_ = r.c.MSet(ctx, entityToCache, constants.NormalCacheDuration) _ = r.c.MSet(ctx, entityToCache, constants.NormalCacheDuration)
} }
if len(ids) > 0 { if len(ids) > 0 {
_ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration) _ = r.c.Set(ctx, queryKey, ids, constants.ListCacheDuration)
} }
@@ -196,15 +205,15 @@ 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),
Description: convert.TextToString(row.Description), Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl), ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status),
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),
} }
go func() {
_ = r.c.DelByPattern(context.Background(), "entity:search*")
}()
return &entity, nil return &entity, nil
} }
@@ -216,8 +225,10 @@ func (r *entityRepository) Update(ctx context.Context, params sqlc.UpdateEntityP
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),
Description: convert.TextToString(row.Description), Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl), ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status),
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),
@@ -234,3 +245,61 @@ func (r *entityRepository) Delete(ctx context.Context, id pgtype.UUID) error {
_ = r.c.Del(ctx, fmt.Sprintf("entity:id:%s", convert.UUIDToString(id))) _ = r.c.Del(ctx, fmt.Sprintf("entity:id:%s", convert.UUIDToString(id)))
return nil return nil
} }
func (r *entityRepository) GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.EntityEntity, error) {
cacheKey := fmt.Sprintf("entity:project:%s", convert.UUIDToString(projectID))
var cachedIDs []string
if err := r.c.Get(ctx, cacheKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs)
}
rows, err := r.q.GetEntitiesByProjectId(ctx, projectID)
if err != nil {
return nil, err
}
var entities []*models.EntityEntity
var ids []string
entityToCache := make(map[string]any)
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),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
ids = append(ids, entity.ID)
entities = append(entities, entity)
entityToCache[fmt.Sprintf("entity:id:%s", entity.ID)] = entity
}
if len(entityToCache) > 0 {
_ = r.c.MSet(ctx, entityToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, cacheKey, ids, constants.ListCacheDuration)
}
return entities, nil
}
func (r *entityRepository) DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error {
err := r.q.DeleteEntitiesByIDs(ctx, ids)
if err != nil {
return err
}
if len(ids) > 0 {
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("entity:id:%s", convert.UUIDToString(id))
}
_ = r.c.Del(ctx, keys...)
}
return nil
}

View File

@@ -26,6 +26,11 @@ type GeometryRepository interface {
Delete(ctx context.Context, id pgtype.UUID) error Delete(ctx context.Context, id pgtype.UUID) error
CreateEntityGeometries(ctx context.Context, params sqlc.CreateEntityGeometriesParams) error CreateEntityGeometries(ctx context.Context, params sqlc.CreateEntityGeometriesParams) error
BulkDeleteEntityGeometriesByEntityId(ctx context.Context, entityId pgtype.UUID) error BulkDeleteEntityGeometriesByEntityId(ctx context.Context, entityId pgtype.UUID) error
GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.GeometryEntity, error)
DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error
BulkDeleteEntityGeometriesByGeometryID(ctx context.Context, geometryID pgtype.UUID) error
DeleteEntityGeometry(ctx context.Context, entityID pgtype.UUID, geometryID pgtype.UUID) error
DeleteEntityGeometriesByProjectID(ctx context.Context, projectID pgtype.UUID) error
WithTx(tx pgx.Tx) GeometryRepository WithTx(tx pgx.Tx) GeometryRepository
} }
@@ -96,6 +101,7 @@ func (r *geometryRepository) getByIDsWithFallback(ctx context.Context, ids []str
MaxLng: row.MaxLng, MaxLng: row.MaxLng,
MaxLat: row.MaxLat, MaxLat: row.MaxLat,
}, },
ProjectID: convert.UUIDToString(row.ProjectID),
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),
@@ -157,6 +163,7 @@ func (r *geometryRepository) GetByID(ctx context.Context, id pgtype.UUID) (*mode
MaxLng: row.MaxLng, MaxLng: row.MaxLng,
MaxLat: row.MaxLat, MaxLat: row.MaxLat,
}, },
ProjectID: convert.UUIDToString(row.ProjectID),
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),
@@ -195,6 +202,7 @@ func (r *geometryRepository) Search(ctx context.Context, params sqlc.SearchGeome
MaxLng: row.MaxLng, MaxLng: row.MaxLng,
MaxLat: row.MaxLat, MaxLat: row.MaxLat,
}, },
ProjectID: convert.UUIDToString(row.ProjectID),
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),
@@ -233,15 +241,12 @@ func (r *geometryRepository) Create(ctx context.Context, params sqlc.CreateGeome
MaxLng: row.MaxLng, MaxLng: row.MaxLng,
MaxLat: row.MaxLat, MaxLat: row.MaxLat,
}, },
ProjectID: convert.UUIDToString(row.ProjectID),
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),
} }
go func() {
_ = r.c.DelByPattern(context.Background(), "geometry:search*")
}()
return &geometry, nil return &geometry, nil
} }
@@ -263,6 +268,7 @@ func (r *geometryRepository) Update(ctx context.Context, params sqlc.UpdateGeome
MaxLng: row.MaxLng, MaxLng: row.MaxLng,
MaxLat: row.MaxLat, MaxLat: row.MaxLat,
}, },
ProjectID: convert.UUIDToString(row.ProjectID),
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),
@@ -281,26 +287,93 @@ func (r *geometryRepository) Delete(ctx context.Context, id pgtype.UUID) error {
} }
func (r *geometryRepository) CreateEntityGeometries(ctx context.Context, params sqlc.CreateEntityGeometriesParams) error { func (r *geometryRepository) CreateEntityGeometries(ctx context.Context, params sqlc.CreateEntityGeometriesParams) error {
err := r.q.CreateEntityGeometries(ctx, params) return r.q.CreateEntityGeometries(ctx, params)
if err != nil { }
return err
} func (r *geometryRepository) DeleteEntityGeometriesByProjectID(ctx context.Context, projectID pgtype.UUID) error {
return err return r.q.DeleteEntityGeometriesByProjectID(ctx, projectID)
} }
func (r *geometryRepository) BulkDeleteEntityGeometriesByEntityId(ctx context.Context, entityId pgtype.UUID) error { func (r *geometryRepository) BulkDeleteEntityGeometriesByEntityId(ctx context.Context, entityId pgtype.UUID) error {
geometryIDs, err := r.q.BulkDeleteEntityGeometriesByEntityId(ctx, entityId) _, err := r.q.BulkDeleteEntityGeometriesByEntityId(ctx, entityId)
if err != nil { if err != nil {
return err return err
} }
if len(geometryIDs) > 0 { return nil
keys := make([]string, len(geometryIDs)) }
for i, id := range geometryIDs {
func (r *geometryRepository) GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.GeometryEntity, error) {
cacheKey := fmt.Sprintf("geometry:project:%s", convert.UUIDToString(projectID))
var cachedIDs []string
if err := r.c.Get(ctx, cacheKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs)
}
rows, err := r.q.GetGeometriesByProjectId(ctx, projectID)
if err != nil {
return nil, err
}
var geometries []*models.GeometryEntity
var ids []string
geometryToCache := make(map[string]any)
for _, row := range rows {
geometry := &models.GeometryEntity{
ID: convert.UUIDToString(row.ID),
GeoType: constants.ParseGeoType(row.GeoType),
DrawGeometry: row.DrawGeometry,
Binding: row.Binding,
TimeStart: convert.Int4ToInt32(row.TimeStart),
TimeEnd: convert.Int4ToInt32(row.TimeEnd),
Bbox: &response.Bbox{
MinLng: row.MinLng,
MinLat: row.MinLat,
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
ProjectID: convert.UUIDToString(row.ProjectID),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
ids = append(ids, geometry.ID)
geometries = append(geometries, geometry)
geometryToCache[fmt.Sprintf("geometry:id:%s", geometry.ID)] = geometry
}
if len(geometryToCache) > 0 {
_ = r.c.MSet(ctx, geometryToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, cacheKey, ids, constants.ListCacheDuration)
}
return geometries, nil
}
func (r *geometryRepository) DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error {
err := r.q.DeleteGeometriesByIDs(ctx, ids)
if err != nil {
return err
}
if len(ids) > 0 {
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("geometry:id:%s", convert.UUIDToString(id)) keys[i] = fmt.Sprintf("geometry:id:%s", convert.UUIDToString(id))
} }
go func() { _ = r.c.Del(ctx, keys...)
_ = r.c.Del(context.Background(), keys...)
}()
} }
return nil return nil
} }
func (r *geometryRepository) BulkDeleteEntityGeometriesByGeometryID(ctx context.Context, geometryID pgtype.UUID) error {
return r.q.BulkDeleteEntityGeometriesByGeometryID(ctx, geometryID)
}
func (r *geometryRepository) DeleteEntityGeometry(ctx context.Context, entityID pgtype.UUID, geometryID pgtype.UUID) error {
return r.q.DeleteEntityGeometry(ctx, sqlc.DeleteEntityGeometryParams{
EntityID: entityID,
GeometryID: geometryID,
})
}

View File

@@ -98,10 +98,10 @@ func (r *projectRepository) getByIDsWithFallback(ctx context.Context, ids []stri
UserID: convert.UUIDToString(row.UserID), UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
} }
_ = item.ParseUser(row.User) _ = item.ParseUser(row.User)
_ = item.ParseCommits(row.Commits) _ = item.ParseCommits(row.Commits)
_ = item.ParseSubmissions(row.Submissions)
_ = item.ParseMembers(row.Members) _ = item.ParseMembers(row.Members)
dbMap[item.ID] = &item dbMap[item.ID] = &item
} }
@@ -158,10 +158,10 @@ func (r *projectRepository) GetByID(ctx context.Context, id pgtype.UUID) (*model
UserID: convert.UUIDToString(row.UserID), UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
} }
_ = project.ParseUser(row.User) _ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits) _ = project.ParseCommits(row.Commits)
_ = project.ParseSubmissions(row.Submissions)
_ = project.ParseMembers(row.Members) _ = project.ParseMembers(row.Members)
_ = r.c.Set(ctx, cacheId, project, constants.NormalCacheDuration) _ = r.c.Set(ctx, cacheId, project, constants.NormalCacheDuration)
@@ -197,10 +197,10 @@ func (r *projectRepository) GetByUserID(ctx context.Context, params sqlc.GetProj
UserID: convert.UUIDToString(row.UserID), UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
} }
_ = project.ParseUser(row.User) _ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits) _ = project.ParseCommits(row.Commits)
_ = project.ParseSubmissions(row.Submissions)
_ = project.ParseMembers(row.Members) _ = project.ParseMembers(row.Members)
ids = append(ids, project.ID) ids = append(ids, project.ID)
@@ -245,10 +245,10 @@ func (r *projectRepository) Search(ctx context.Context, params sqlc.SearchProjec
UserID: convert.UUIDToString(row.UserID), UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
} }
_ = project.ParseUser(row.User) _ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits) _ = project.ParseCommits(row.Commits)
_ = project.ParseSubmissions(row.Submissions)
_ = project.ParseMembers(row.Members) _ = project.ParseMembers(row.Members)
ids = append(ids, project.ID) ids = append(ids, project.ID)
@@ -299,10 +299,10 @@ func (r *projectRepository) Create(ctx context.Context, params sqlc.CreateProjec
UserID: convert.UUIDToString(row.UserID), UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
} }
_ = project.ParseUser(row.User) _ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits) _ = project.ParseCommits(row.Commits)
_ = project.ParseSubmissions(row.Submissions)
_ = project.ParseMembers(row.Members) _ = project.ParseMembers(row.Members)
go func() { go func() {
@@ -330,10 +330,10 @@ func (r *projectRepository) Update(ctx context.Context, params sqlc.UpdateProjec
UserID: convert.UUIDToString(row.UserID), UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt), CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt), UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
} }
_ = project.ParseUser(row.User) _ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits) _ = project.ParseCommits(row.Commits)
_ = project.ParseSubmissions(row.Submissions)
_ = project.ParseMembers(row.Members) _ = project.ParseMembers(row.Members)
_ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", project.ID)) _ = r.c.Del(ctx, fmt.Sprintf("project:id:%s", project.ID))

View File

@@ -25,6 +25,11 @@ type WikiRepository interface {
Delete(ctx context.Context, id pgtype.UUID) error Delete(ctx context.Context, id pgtype.UUID) error
CreateEntityWikis(ctx context.Context, params sqlc.CreateEntityWikisParams) error CreateEntityWikis(ctx context.Context, params sqlc.CreateEntityWikisParams) error
BulkDeleteEntityWikisByEntityId(ctx context.Context, entityId pgtype.UUID) error BulkDeleteEntityWikisByEntityId(ctx context.Context, entityId pgtype.UUID) error
GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.WikiEntity, error)
DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error
BulkDeleteEntityWikisByWikiID(ctx context.Context, wikiID pgtype.UUID) error
DeleteEntityWiki(ctx context.Context, entityID pgtype.UUID, wikiID pgtype.UUID) error
DeleteEntityWikisByProjectID(ctx context.Context, projectID pgtype.UUID) error
WithTx(tx pgx.Tx) WikiRepository WithTx(tx pgx.Tx) WikiRepository
} }
@@ -85,8 +90,9 @@ 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),
Content: convert.TextToString(row.Content), Content: json.RawMessage(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),
} }
@@ -137,7 +143,7 @@ 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),
Content: convert.TextToString(row.Content), Content: json.RawMessage(row.Content),
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),
@@ -166,7 +172,7 @@ 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),
Content: convert.TextToString(row.Content), Content: json.RawMessage(row.Content),
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),
@@ -195,16 +201,12 @@ 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),
Content: convert.TextToString(row.Content), Content: json.RawMessage(row.Content),
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),
} }
go func() {
_ = r.c.DelByPattern(context.Background(), "wiki:search*")
}()
return &wiki, nil return &wiki, nil
} }
@@ -216,7 +218,7 @@ 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),
Content: convert.TextToString(row.Content), Content: json.RawMessage(row.Content),
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),
@@ -235,26 +237,84 @@ func (r *wikiRepository) Delete(ctx context.Context, id pgtype.UUID) error {
} }
func (r *wikiRepository) CreateEntityWikis(ctx context.Context, params sqlc.CreateEntityWikisParams) error { func (r *wikiRepository) CreateEntityWikis(ctx context.Context, params sqlc.CreateEntityWikisParams) error {
err := r.q.CreateEntityWikis(ctx, params) return r.q.CreateEntityWikis(ctx, params)
}
func (r *wikiRepository) DeleteEntityWikisByProjectID(ctx context.Context, projectID pgtype.UUID) error {
return r.q.DeleteEntityWikisByProjectID(ctx, projectID)
}
func (r *wikiRepository) BulkDeleteEntityWikisByEntityId(ctx context.Context, entityId pgtype.UUID) error {
_, err := r.q.BulkDeleteEntityWikisByEntityId(ctx, entityId)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (r *wikiRepository) BulkDeleteEntityWikisByEntityId(ctx context.Context, entityId pgtype.UUID) error { func (r *wikiRepository) GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.WikiEntity, error) {
wikiIDs, err := r.q.BulkDeleteEntityWikisByEntityId(ctx, entityId) cacheKey := fmt.Sprintf("wiki:project:%s", convert.UUIDToString(projectID))
var cachedIDs []string
if err := r.c.Get(ctx, cacheKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
return r.getByIDsWithFallback(ctx, cachedIDs)
}
rows, err := r.q.GetWikisByProjectId(ctx, projectID)
if err != nil {
return nil, err
}
var wikis []*models.WikiEntity
var ids []string
wikiToCache := make(map[string]any)
for _, row := range rows {
wiki := &models.WikiEntity{
ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title),
Content: json.RawMessage(row.Content),
IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
ids = append(ids, wiki.ID)
wikis = append(wikis, wiki)
wikiToCache[fmt.Sprintf("wiki:id:%s", wiki.ID)] = wiki
}
if len(wikiToCache) > 0 {
_ = r.c.MSet(ctx, wikiToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = r.c.Set(ctx, cacheKey, ids, constants.ListCacheDuration)
}
return wikis, nil
}
func (r *wikiRepository) DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error {
err := r.q.DeleteWikisByIDs(ctx, ids)
if err != nil { if err != nil {
return err return err
} }
if len(wikiIDs) > 0 { if len(ids) > 0 {
keys := make([]string, len(wikiIDs)) keys := make([]string, len(ids))
for i, id := range wikiIDs { for i, id := range ids {
keys[i] = fmt.Sprintf("wiki:id:%s", convert.UUIDToString(id)) keys[i] = fmt.Sprintf("wiki:id:%s", convert.UUIDToString(id))
} }
go func() { _ = r.c.Del(ctx, keys...)
_ = r.c.Del(context.Background(), keys...)
}()
} }
return nil return nil
} }
func (r *wikiRepository) BulkDeleteEntityWikisByWikiID(ctx context.Context, wikiID pgtype.UUID) error {
return r.q.BulkDeleteEntityWikisByWikiID(ctx, wikiID)
}
func (r *wikiRepository) DeleteEntityWiki(ctx context.Context, entityID pgtype.UUID, wikiID pgtype.UUID) error {
return r.q.DeleteEntityWiki(ctx, sqlc.DeleteEntityWikiParams{
EntityID: entityID,
WikiID: wikiID,
})
}

View File

@@ -2,6 +2,7 @@ package services
import ( import (
"context" "context"
"encoding/json"
"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"
@@ -63,6 +64,12 @@ func (s *commitService) checkWritePermission(ctx context.Context, userID string,
return fiber.NewError(fiber.StatusForbidden, "You do not have permission to write to this project") return fiber.NewError(fiber.StatusForbidden, "You do not have permission to write to this project")
} }
for _, s := range project.Submissions {
if s.Status == constants.StatusTypePending {
return fiber.NewError(fiber.StatusConflict, "Cannot create commit while there is a pending submission")
}
}
return nil return nil
} }
@@ -90,9 +97,14 @@ func (s *commitService) CreateCommit(ctx context.Context, userID string, project
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid user ID") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid user ID")
} }
snapshotJSON, err := json.Marshal(dto.SnapshotJson)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid snapshot JSON")
}
commit, err := cRepoTx.Create(ctx, sqlc.CreateCommitParams{ commit, err := cRepoTx.Create(ctx, sqlc.CreateCommitParams{
ProjectID: projectUUID, ProjectID: projectUUID,
SnapshotJson: dto.SnapshotJson, SnapshotJson: snapshotJSON,
UserID: userUUID, UserID: userUUID,
EditSummary: convert.StringToText(dto.EditSummary), EditSummary: convert.StringToText(dto.EditSummary),
}) })

View File

@@ -59,6 +59,13 @@ func (s *entityService) SearchEntities(ctx context.Context, req *request.SearchE
params.Name = req.Name params.Name = req.Name
} }
if req.ProjectID != nil {
projectID, err := convert.StringToUUID(*req.ProjectID)
if err == nil {
params.ProjectID = projectID
}
}
entities, err := s.entityRepo.Search(ctx, params) entities, err := s.entityRepo.Search(ctx, params)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to search entities") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to search entities")

View File

@@ -2,6 +2,7 @@ package services
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"history-api/internal/dtos/request" "history-api/internal/dtos/request"
"history-api/internal/dtos/response" "history-api/internal/dtos/response"
@@ -32,6 +33,9 @@ type submissionService struct {
projectRepo repositories.ProjectRepository projectRepo repositories.ProjectRepository
commitRepo repositories.CommitRepository commitRepo repositories.CommitRepository
userRepo repositories.UserRepository userRepo repositories.UserRepository
wikiRepo repositories.WikiRepository
geometryRepo repositories.GeometryRepository
entityRepo repositories.EntityRepository
db *pgxpool.Pool db *pgxpool.Pool
c cache.Cache c cache.Cache
} }
@@ -41,6 +45,9 @@ func NewSubmissionService(
projectRepo repositories.ProjectRepository, projectRepo repositories.ProjectRepository,
commitRepo repositories.CommitRepository, commitRepo repositories.CommitRepository,
userRepo repositories.UserRepository, userRepo repositories.UserRepository,
wikiRepo repositories.WikiRepository,
geometryRepo repositories.GeometryRepository,
entityRepo repositories.EntityRepository,
db *pgxpool.Pool, db *pgxpool.Pool,
c cache.Cache, c cache.Cache,
) SubmissionService { ) SubmissionService {
@@ -49,6 +56,9 @@ func NewSubmissionService(
projectRepo: projectRepo, projectRepo: projectRepo,
commitRepo: commitRepo, commitRepo: commitRepo,
userRepo: userRepo, userRepo: userRepo,
wikiRepo: wikiRepo,
geometryRepo: geometryRepo,
entityRepo: entityRepo,
db: db, db: db,
c: c, c: c,
} }
@@ -78,6 +88,17 @@ 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")
} }
project, err := s.projectRepo.GetByID(ctx, projectUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Project not found")
}
for _, sub := range project.Submissions {
if sub.Status == constants.StatusTypePending {
return nil, fiber.NewError(fiber.StatusConflict, "There is already a pending submission for this project")
}
}
arg := sqlc.CreateSubmissionParams{ arg := sqlc.CreateSubmissionParams{
ProjectID: projectUUID, ProjectID: projectUUID,
CommitID: commitUUID, CommitID: commitUUID,
@@ -95,6 +116,18 @@ func (s *submissionService) CreateSubmission(ctx context.Context, userID string,
} }
func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewerID string, submissionID string, dto *request.UpdateSubmissionStatusDto) (*response.SubmissionResponse, *fiber.Error) { func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewerID string, submissionID string, dto *request.UpdateSubmissionStatusDto) (*response.SubmissionResponse, *fiber.Error) {
tx, err := s.db.Begin(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction")
}
defer tx.Rollback(ctx)
submissionRepo := s.submissionRepo.WithTx(tx)
commitRepo := s.commitRepo.WithTx(tx)
entityRepo := s.entityRepo.WithTx(tx)
geometryRepo := s.geometryRepo.WithTx(tx)
wikiRepo := s.wikiRepo.WithTx(tx)
submissionUUID, err := convert.StringToUUID(submissionID) submissionUUID, err := convert.StringToUUID(submissionID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid submission ID") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid submission ID")
@@ -119,6 +152,404 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
return nil, fiber.NewError(fiber.StatusBadRequest, "Submission already processed") return nil, fiber.NewError(fiber.StatusBadRequest, "Submission already processed")
} }
commitUUID, err := convert.StringToUUID(submission.CommitID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid commit ID")
}
commit, err := s.commitRepo.GetByID(ctx, commitUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Commit not found")
}
if commit.ProjectID != submission.ProjectID {
return nil, fiber.NewError(fiber.StatusBadRequest, "Commit does not belong to project")
}
if status == constants.StatusTypeApproved {
var snapshotData request.CommitSnapshot
err = json.Unmarshal(commit.SnapshotJson, &snapshotData)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to parse commit snapshot")
}
projectUUID, err := convert.StringToUUID(commit.ProjectID)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project ID")
}
currentEntity, err := s.entityRepo.GetByProjectID(ctx, projectUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Entity not found")
}
currentGeometry, err := s.geometryRepo.GetByProjectID(ctx, projectUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Geometry not found")
}
currentWiki, err := s.wikiRepo.GetByProjectID(ctx, projectUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusNotFound, "Wiki not found")
}
persistItemIDs := make(map[string]struct{})
for _, item := range snapshotData.Entities {
persistItemIDs[item.ID] = struct{}{}
}
for _, item := range snapshotData.Geometries {
persistItemIDs[item.ID] = struct{}{}
}
for _, item := range snapshotData.Wikis {
persistItemIDs[item.ID] = struct{}{}
}
persistCurrentItemIDs := make(map[string]struct{})
for _, item := range currentEntity {
persistCurrentItemIDs[item.ID] = struct{}{}
}
for _, item := range currentGeometry {
persistCurrentItemIDs[item.ID] = struct{}{}
}
for _, item := range currentWiki {
persistCurrentItemIDs[item.ID] = struct{}{}
}
listDeleteEntities := make([]pgtype.UUID, 0)
for _, e := range currentEntity {
if _, ok := persistItemIDs[e.ID]; !ok {
itemUUID, err := convert.StringToUUID(e.ID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid entity ID")
}
listDeleteEntities = append(listDeleteEntities, itemUUID)
delete(persistCurrentItemIDs, e.ID)
}
}
listDeleteGeometries := make([]pgtype.UUID, 0)
for _, g := range currentGeometry {
if _, ok := persistItemIDs[g.ID]; !ok {
itemUUID, err := convert.StringToUUID(g.ID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid geometry ID")
}
listDeleteGeometries = append(listDeleteGeometries, itemUUID)
delete(persistCurrentItemIDs, g.ID)
}
}
listDeleteWikis := make([]pgtype.UUID, 0)
for _, w := range currentWiki {
if _, ok := persistItemIDs[w.ID]; !ok {
itemUUID, err := convert.StringToUUID(w.ID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid wiki ID")
}
listDeleteWikis = append(listDeleteWikis, itemUUID)
delete(persistCurrentItemIDs, w.ID)
}
}
if len(listDeleteEntities) > 0 {
if err = entityRepo.DeleteByIDs(ctx, listDeleteEntities); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete entities")
}
}
if len(listDeleteGeometries) > 0 {
if err = geometryRepo.DeleteByIDs(ctx, listDeleteGeometries); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete geometries")
}
}
if len(listDeleteWikis) > 0 {
if err = wikiRepo.DeleteByIDs(ctx, listDeleteWikis); err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete wikis")
}
}
refEntityIDs := []string{}
for _, e := range snapshotData.Entities {
if e.Source == "ref" {
refEntityIDs = append(refEntityIDs, e.ID)
}
}
refEntities, _ := s.entityRepo.GetByIDs(ctx, refEntityIDs)
refEntityMap := make(map[string]bool)
for _, e := range refEntities {
refEntityMap[e.ID] = true
}
newEntities := make([]*request.EntitySnapshot, 0, len(snapshotData.Entities))
for i, entity := range snapshotData.Entities {
if entity.Operation == "delete" {
continue
}
entityUUID, err := convert.StringToUUID(entity.ID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid entity ID")
}
if _, ok := persistCurrentItemIDs[entity.ID]; ok {
_, err := entityRepo.Update(ctx, sqlc.UpdateEntityParams{
Name: convert.StringToText(entity.Name),
Description: convert.StringToText(entity.Description),
Slug: convert.PtrToText(entity.Slug),
Status: convert.PtrToInt2(entity.Status),
ID: entityUUID,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update entity: "+entity.ID)
}
newEntities = append(newEntities, snapshotData.Entities[i])
} else if entity.Source == "inline" {
_, err := entityRepo.Create(ctx, sqlc.CreateEntityParams{
ID: entityUUID,
Name: entity.Name,
Description: convert.StringToText(entity.Description),
ProjectID: projectUUID,
Slug: convert.PtrToText(entity.Slug),
Status: convert.PtrToInt2(entity.Status),
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create entity: "+entity.ID)
}
newEntities = append(newEntities, snapshotData.Entities[i])
} else if entity.Source == "ref" {
if !refEntityMap[entity.ID] {
continue
}
newEntities = append(newEntities, snapshotData.Entities[i])
}
}
snapshotData.Entities = newEntities
refGeometryIDs := []string{}
for _, g := range snapshotData.Geometries {
if g.Source == "ref" {
refGeometryIDs = append(refGeometryIDs, g.ID)
}
}
refGeometries, _ := s.geometryRepo.GetByIDs(ctx, refGeometryIDs)
refGeometryMap := make(map[string]bool)
for _, g := range refGeometries {
refGeometryMap[g.ID] = true
}
newGeometries := make([]*request.GeometrySnapshot, 0, len(snapshotData.Geometries))
for i, geo := range snapshotData.Geometries {
if geo.Operation == "delete" {
continue
}
geometryUUID, err := convert.StringToUUID(geo.ID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid geometry ID")
}
binding, _ := json.Marshal(geo.Binding)
if _, ok := persistCurrentItemIDs[geo.ID]; ok {
params := sqlc.UpdateGeometryParams{
ID: geometryUUID,
GeoType: pgtype.Int2{Int16: constants.ParseGeoTypeText(geo.Type).Int16(), Valid: true},
DrawGeometry: geo.DrawGeometry,
Binding: binding,
TimeStart: convert.PtrFloat64ToInt4(geo.TimeStart),
TimeEnd: convert.PtrFloat64ToInt4(geo.TimeEnd),
ProjectID: projectUUID,
}
if geo.BBox != nil {
params.UpdateBbox = pgtype.Bool{Bool: true, Valid: true}
params.MinLng = pgtype.Float8{Float64: geo.BBox.MinLng, Valid: true}
params.MinLat = pgtype.Float8{Float64: geo.BBox.MinLat, Valid: true}
params.MaxLng = pgtype.Float8{Float64: geo.BBox.MaxLng, Valid: true}
params.MaxLat = pgtype.Float8{Float64: geo.BBox.MaxLat, Valid: true}
}
_, err := geometryRepo.Update(ctx, params)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update geometry: "+geo.ID)
}
newGeometries = append(newGeometries, snapshotData.Geometries[i])
} else if geo.Source == "inline" {
params := sqlc.CreateGeometryParams{
ID: geometryUUID,
GeoType: constants.ParseGeoTypeText(geo.Type).Int16(),
DrawGeometry: geo.DrawGeometry,
Binding: binding,
TimeStart: convert.PtrFloat64ToInt4(geo.TimeStart),
TimeEnd: convert.PtrFloat64ToInt4(geo.TimeEnd),
ProjectID: projectUUID,
}
if geo.BBox != nil {
params.MinLng = geo.BBox.MinLng
params.MinLat = geo.BBox.MinLat
params.MaxLng = geo.BBox.MaxLng
params.MaxLat = geo.BBox.MaxLat
}
_, err := geometryRepo.Create(ctx, params)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create geometry: "+geo.ID)
}
newGeometries = append(newGeometries, snapshotData.Geometries[i])
} else if geo.Source == "ref" {
if !refGeometryMap[geo.ID] {
continue
}
newGeometries = append(newGeometries, snapshotData.Geometries[i])
}
}
snapshotData.Geometries = newGeometries
refWikiIDs := []string{}
for _, w := range snapshotData.Wikis {
if w.Source == "ref" {
refWikiIDs = append(refWikiIDs, w.ID)
}
}
refWikis, _ := s.wikiRepo.GetByIDs(ctx, refWikiIDs)
refWikiMap := make(map[string]bool)
for _, w := range refWikis {
refWikiMap[w.ID] = true
}
newWikis := make([]*request.WikiSnapshot, 0, len(snapshotData.Wikis))
for i, wiki := range snapshotData.Wikis {
if wiki.Operation == "delete" {
continue
}
wikiUUID, err := convert.StringToUUID(wiki.ID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Invalid wiki ID")
}
if _, ok := persistCurrentItemIDs[wiki.ID]; ok {
_, err := wikiRepo.Update(ctx, sqlc.UpdateWikiParams{
ID: wikiUUID,
Title: convert.StringToText(wiki.Title),
Content: wiki.Doc,
ProjectID: projectUUID,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update wiki: "+wiki.ID)
}
newWikis = append(newWikis, snapshotData.Wikis[i])
} else if wiki.Source == "inline" {
_, err := wikiRepo.Create(ctx, sqlc.CreateWikiParams{
ID: wikiUUID,
Title: convert.StringToText(wiki.Title),
Content: wiki.Doc,
ProjectID: projectUUID,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki: "+wiki.ID)
}
newWikis = append(newWikis, snapshotData.Wikis[i])
} else if wiki.Source == "ref" {
if !refWikiMap[wiki.ID] {
continue
}
newWikis = append(newWikis, snapshotData.Wikis[i])
}
}
snapshotData.Wikis = newWikis
err = geometryRepo.DeleteEntityGeometriesByProjectID(ctx, projectUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete geometry entity: "+err.Error())
}
err = wikiRepo.DeleteEntityWikisByProjectID(ctx, projectUUID)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete wiki entity: "+err.Error())
}
validEntities := make(map[string]bool)
for _, e := range snapshotData.Entities {
validEntities[e.ID] = true
}
validGeometries := make(map[string]bool)
for _, g := range snapshotData.Geometries {
validGeometries[g.ID] = true
}
validWikis := make(map[string]bool)
for _, w := range snapshotData.Wikis {
validWikis[w.ID] = true
}
if len(snapshotData.GeometryEntity) > 0 {
geomLinks := make(map[string][]pgtype.UUID)
for _, link := range snapshotData.GeometryEntity {
if !validEntities[link.EntityID] || !validGeometries[link.GeometryID] {
continue
}
gID, _ := convert.StringToUUID(link.GeometryID)
geomLinks[link.EntityID] = append(geomLinks[link.EntityID], gID)
}
for eIDStr, gIDs := range geomLinks {
eID, _ := convert.StringToUUID(eIDStr)
err = geometryRepo.CreateEntityGeometries(ctx, sqlc.CreateEntityGeometriesParams{
EntityID: eID,
GeometryIds: gIDs,
ProjectID: projectUUID,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create geometry entity: "+err.Error())
}
}
}
if len(snapshotData.EntityWiki) > 0 {
wikiLinks := make(map[string][]pgtype.UUID)
for _, link := range snapshotData.EntityWiki {
if link.Operation == "delete" || (link.IsDeleted != nil && *link.IsDeleted == 1) {
continue
}
if !validEntities[link.EntityID] || !validWikis[link.WikiID] {
continue
}
wID, _ := convert.StringToUUID(link.WikiID)
wikiLinks[link.EntityID] = append(wikiLinks[link.EntityID], wID)
}
for eIDStr, wIDs := range wikiLinks {
eID, _ := convert.StringToUUID(eIDStr)
err = wikiRepo.CreateEntityWikis(ctx, sqlc.CreateEntityWikisParams{
EntityID: eID,
WikiIds: wIDs,
ProjectID: projectUUID,
})
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create wiki entity: "+err.Error())
}
}
}
newSnapshot, err := json.Marshal(snapshotData)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal snapshot")
}
_, err = commitRepo.UpdateSnapshot(ctx, commitUUID, newSnapshot)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update snapshot: "+err.Error())
}
}
arg := sqlc.UpdateSubmissionParams{ arg := sqlc.UpdateSubmissionParams{
ID: submissionUUID, ID: submissionUUID,
Status: pgtype.Int2{Int16: status.Int16(), Valid: true}, Status: pgtype.Int2{Int16: status.Int16(), Valid: true},
@@ -126,11 +557,25 @@ func (s *submissionService) UpdateSubmissionStatus(ctx context.Context, reviewer
ReviewNote: convert.StringToText(dto.ReviewNote), ReviewNote: convert.StringToText(dto.ReviewNote),
} }
updatedSubmission, err := s.submissionRepo.Update(ctx, arg) updatedSubmission, err := submissionRepo.Update(ctx, arg)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update submission status") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update submission status")
} }
err = tx.Commit(ctx)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction")
}
if status == constants.StatusTypeApproved {
go func() {
bgCtx := context.Background()
_ = s.c.DelByPattern(bgCtx, "entity:search*")
_ = s.c.DelByPattern(bgCtx, "geometry:search*")
_ = s.c.DelByPattern(bgCtx, "wiki:search*")
}()
}
_ = s.c.Del(ctx, fmt.Sprintf("project:id:%s", submission.ProjectID)) _ = s.c.Del(ctx, fmt.Sprintf("project:id:%s", submission.ProjectID))
return updatedSubmission.ToResponse(), nil return updatedSubmission.ToResponse(), nil

View File

@@ -58,6 +58,7 @@ func (s *wikiService) SearchWikis(ctx context.Context, req *request.SearchWikiDt
if req.Title != "" { if req.Title != "" {
params.Title = req.Title params.Title = req.Title
} }
if req.EntityID != "" { if req.EntityID != "" {
entityId, err := convert.StringToUUID(req.EntityID) entityId, err := convert.StringToUUID(req.EntityID)
if err == nil { if err == nil {
@@ -65,6 +66,13 @@ func (s *wikiService) SearchWikis(ctx context.Context, req *request.SearchWikiDt
} }
} }
if req.ProjectID != nil {
projectID, err := convert.StringToUUID(*req.ProjectID)
if err == nil {
params.ProjectID = projectID
}
}
wikis, err := s.wikiRepo.Search(ctx, params) wikis, err := s.wikiRepo.Search(ctx, params)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to search wikis") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to search wikis")

View File

@@ -101,6 +101,16 @@ func TextToPtr(v pgtype.Text) *string {
return &v.String return &v.String
} }
func PtrToInt4(i *int32) pgtype.Int4 {
if i == nil {
return pgtype.Int4{Valid: false}
}
return pgtype.Int4{
Int32: *i,
Valid: true,
}
}
func Int4ToPtr(v pgtype.Int4) *int32 { func Int4ToPtr(v pgtype.Int4) *int32 {
if !v.Valid { if !v.Valid {
return nil return nil
@@ -114,3 +124,31 @@ func Int4ToInt32(v pgtype.Int4) int32 {
} }
return 0 return 0
} }
func PtrToInt2(v *int) pgtype.Int2 {
if v == nil {
return pgtype.Int2{Valid: false}
}
return pgtype.Int2{
Int16: int16(*v),
Valid: true,
}
}
func Int2ToInt16Ptr(v pgtype.Int2) *int16 {
if !v.Valid {
return nil
}
int16Val := v.Int16
return &int16Val
}
func PtrFloat64ToInt4(v *float64) pgtype.Int4 {
if v == nil {
return pgtype.Int4{Valid: false}
}
return pgtype.Int4{
Int32: int32(*v),
Valid: true,
}
}