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)
projectService := services.NewProjectService(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
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

@@ -23,3 +23,6 @@ ON projects (project_status, updated_at DESC);
CREATE INDEX idx_projects_title_trgm
ON projects USING GIN (title gin_trgm_ops);

View File

@@ -1,17 +1,21 @@
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,
thumbnail_url TEXT,
status SMALLINT,
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_entities_name_search
ON entities USING GIN (name gin_trgm_ops);
CREATE INDEX idx_entities_project_id ON entities(project_id);
CREATE INDEX idx_entities_created_active
ON entities(created_at DESC)
WHERE is_deleted = false;

View File

@@ -2,8 +2,9 @@ CREATE EXTENSION IF NOT EXISTS pg_trgm;
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 TEXT,
content JSONB,
is_deleted BOOLEAN NOT NULL DEFAULT false,
created_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 (
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 INDEX idx_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)
WHERE is_deleted = false;
CREATE INDEX idx_wikis_project_id ON wikis(project_id);
CREATE TRIGGER trigger_wikis_updated_at
BEFORE UPDATE ON wikis
FOR EACH ROW

View File

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

View File

@@ -34,3 +34,9 @@ LIMIT sqlc.arg('limit');
-- name: GetCommitsByIDs :many
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
INSERT INTO entities (
name, description, thumbnail_url
id, name, slug, description, project_id, status
) VALUES (
$1, $2, $3
COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3, $4, $5
)
RETURNING *;
@@ -17,8 +17,10 @@ WHERE id = $1 AND is_deleted = false;
UPDATE entities
SET
name = COALESCE(sqlc.narg('name'), name),
slug = COALESCE(sqlc.narg('slug'), slug),
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
RETURNING *;
@@ -34,6 +36,7 @@ WHERE id = $1;
SELECT *
FROM entities
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 (sqlc.narg('cursor_id')::uuid IS NULL OR id < sqlc.narg('cursor_id')::uuid)
ORDER BY id DESC
@@ -41,3 +44,13 @@ LIMIT sqlc.arg('limit_count');
-- name: GetEntitiesByIDs :many
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
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 (
$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,
is_deleted, created_at, updated_at;
-- 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,
is_deleted, created_at, updated_at
FROM geometries
@@ -23,6 +23,7 @@ SET
binding = COALESCE(sqlc.narg('binding'), binding),
time_start = COALESCE(sqlc.narg('time_start'), time_start),
time_end = COALESCE(sqlc.narg('time_end'), time_end),
project_id = COALESCE(sqlc.narg('project_id'), project_id),
bbox = CASE
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)
@@ -30,7 +31,7 @@ SET
END,
updated_at = now()
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,
is_deleted, created_at, updated_at;
@@ -42,7 +43,7 @@ WHERE id = $1;
-- name: SearchGeometries :many
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_YMin(g.bbox)::float8 as min_lat,
ST_XMax(g.bbox)::float8 as max_lng,
@@ -50,6 +51,7 @@ SELECT
g.is_deleted, g.created_at, g.updated_at
FROM geometries g
WHERE g.is_deleted = false
AND (sqlc.narg('project_id')::uuid IS NULL OR g.project_id = sqlc.narg('project_id')::uuid)
AND (
sqlc.narg('search_min_lng')::float8 IS NULL OR
sqlc.narg('search_min_lat')::float8 IS NULL OR
@@ -85,13 +87,18 @@ RETURNING geometry_id;
-- name: CreateEntityGeometries :exec
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
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_YMin(bbox)::float8 as min_lat,
ST_XMax(bbox)::float8 as max_lng,
@@ -99,3 +106,27 @@ SELECT
is_deleted, created_at, updated_at
FROM geometries
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
)::json AS user,
'[]'::json AS commits,
'{}'::uuid[] AS submission_ids,
'[]'::json AS submissions,
'[]'::json AS members
FROM inserted_project p
JOIN users u ON p.user_id = u.id
@@ -32,9 +32,9 @@ SELECT
'[]'
)::json AS commits,
COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id),
'{}'
)::uuid[] AS submission_ids,
(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(
'id', u.id,
'email', u.email,
@@ -85,7 +85,10 @@ RETURNING
FROM commits c WHERE c.project_id = projects.id AND c.is_deleted = false),
'[]'
)::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(
(SELECT json_agg(json_build_object(
'user_id', pm.user_id, 'role', pm.role,
@@ -113,9 +116,9 @@ SELECT
'[]'
)::json AS commits,
COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id),
'{}'
)::uuid[] AS submission_ids,
(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(
'id', u.id,
'email', u.email,
@@ -187,9 +190,9 @@ SELECT
'[]'
)::json AS commits,
COALESCE(
(SELECT array_agg(id) FROM submissions WHERE project_id = p.id),
'{}'
)::uuid[] AS submission_ids,
(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(
'id', u.id,
'email', u.email,
@@ -225,7 +228,10 @@ SELECT
FROM commits c WHERE c.project_id = p.id AND c.is_deleted = false),
'[]'
)::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(
'id', u.id,
'email', u.email,

View File

@@ -1,8 +1,8 @@
-- name: CreateWiki :one
INSERT INTO wikis (
title, content
id, title, content, project_id
) VALUES (
$1, $2
COALESCE(sqlc.narg('id')::uuid, uuidv7()), $1, $2, $3
)
RETURNING *;
@@ -15,7 +15,8 @@ WHERE id = $1 AND is_deleted = false;
UPDATE wikis
SET
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
RETURNING *;
@@ -25,10 +26,12 @@ SET
is_deleted = true
WHERE id = $1;
-- name: SearchWikis :many
SELECT w.*
FROM wikis w
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 (
sqlc.narg('entity_id')::uuid IS NULL OR
@@ -51,10 +54,33 @@ RETURNING wiki_id;
-- name: CreateEntityWikis :exec
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
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)
);
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 (
id UUID PRIMARY KEY DEFAULT uuidv7(),
title TEXT NOT NULL,
@@ -128,6 +83,60 @@ CREATE TABLE IF NOT EXISTS projects (
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 (
id UUID PRIMARY KEY DEFAULT uuidv7(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,

View File

@@ -430,6 +430,11 @@ const docTemplate = `{
"type": "string",
"name": "name",
"in": "query"
},
{
"type": "string",
"name": "project_id",
"in": "query"
}
],
"responses": {
@@ -537,6 +542,11 @@ const docTemplate = `{
"in": "query",
"required": true
},
{
"type": "string",
"name": "project_id",
"in": "query"
},
{
"type": "integer",
"name": "time",
@@ -3418,6 +3428,11 @@ const docTemplate = `{
"name": "limit",
"in": "query"
},
{
"type": "string",
"name": "project_id",
"in": "query"
},
{
"maxLength": 1000,
"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": {
"type": "object",
"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": {
"type": "object",
"required": [
@@ -3556,10 +3638,7 @@ const docTemplate = `{
"maxLength": 500
},
"snapshot_json": {
"type": "array",
"items": {
"type": "integer"
}
"$ref": "#/definitions/history-api_internal_dtos_request.CommitSnapshot"
}
}
},
@@ -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": {
"type": "object",
"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": {
"type": "object",
"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": {
"type": "object",
"properties": {

View File

@@ -423,6 +423,11 @@
"type": "string",
"name": "name",
"in": "query"
},
{
"type": "string",
"name": "project_id",
"in": "query"
}
],
"responses": {
@@ -530,6 +535,11 @@
"in": "query",
"required": true
},
{
"type": "string",
"name": "project_id",
"in": "query"
},
{
"type": "integer",
"name": "time",
@@ -3411,6 +3421,11 @@
"name": "limit",
"in": "query"
},
{
"type": "string",
"name": "project_id",
"in": "query"
},
{
"maxLength": 1000,
"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": {
"type": "object",
"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": {
"type": "object",
"required": [
@@ -3549,10 +3631,7 @@
"maxLength": 500
},
"snapshot_json": {
"type": "array",
"items": {
"type": "integer"
}
"$ref": "#/definitions/history-api_internal_dtos_request.CommitSnapshot"
}
}
},
@@ -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": {
"type": "object",
"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": {
"type": "object",
"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": {
"type": "object",
"properties": {

View File

@@ -13,6 +13,22 @@ definitions:
- role
- user_id
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:
properties:
new_owner_id:
@@ -43,15 +59,42 @@ definitions:
required:
- role_ids
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:
properties:
edit_summary:
maxLength: 500
type: string
snapshot_json:
items:
type: integer
type: array
$ref: '#/definitions/history-api_internal_dtos_request.CommitSnapshot'
required:
- edit_summary
- snapshot_json
@@ -143,6 +186,122 @@ definitions:
- content
- verify_type
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:
properties:
email:
@@ -160,6 +319,58 @@ definitions:
- new_password
- token_id
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:
properties:
media_ids:
@@ -316,6 +527,34 @@ definitions:
- token
- token_type
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:
properties:
data: {}
@@ -636,6 +875,9 @@ paths:
maxLength: 255
name: name
type: string
- in: query
name: project_id
type: string
produces:
- application/json
responses:
@@ -708,6 +950,9 @@ paths:
name: min_lng
required: true
type: number
- in: query
name: project_id
type: string
- in: query
name: time
type: integer
@@ -2565,6 +2810,9 @@ paths:
minimum: 1
name: limit
type: integer
- in: query
name: project_id
type: string
- in: query
maxLength: 1000
name: title

View File

@@ -1,9 +1,7 @@
package request
import "encoding/json"
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"`
}

View File

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

View File

@@ -2,6 +2,7 @@ package request
type SearchWikiDto struct {
Cursor string `json:"cursor" query:"cursor" validate:"omitempty,uuid"`
ProjectID *string `json:"project_id" query:"project_id" validate:"omitempty,uuid"`
Limit int `json:"limit" query:"limit" validate:"omitempty,min=1,max=100"`
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 {
ID string `json:"id"`
Name string `json:"name"`
Slug string `json:"slug,omitempty"`
Description string `json:"description,omitempty"`
ThumbnailUrl string `json:"thumbnail_url,omitempty"`
IsDeleted bool `json:"is_deleted"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
ProjectID string `json:"project_id"`
Status *int16 `json:"status,omitempty"`
IsDeleted bool `json:"is_deleted,omitempty"`
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"`
TimeEnd int32 `json:"time_end,omitempty"`
Bbox *Bbox `json:"bbox,omitempty"`
ProjectID string `json:"project_id"`
IsDeleted bool `json:"is_deleted,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`

View File

@@ -14,6 +14,11 @@ type MemberSimpleResponse struct {
AvatarUrl string `json:"avatar_url"`
}
type SubmissionSimpleResponse struct {
ID string `json:"id"`
Status string `json:"status"`
}
type ProjectResponse struct {
ID string `json:"id"`
Title string `json:"title"`
@@ -27,6 +32,6 @@ type ProjectResponse struct {
UpdatedAt *time.Time `json:"updated_at"`
User *UserSimpleResponse `json:"user,omitempty"`
Commits []CommitSimpleResponse `json:"commits"`
SubmissionIds []string `json:"submission_ids"`
Submissions []SubmissionSimpleResponse `json:"submissions"`
Members []MemberSimpleResponse `json:"members"`
}

View File

@@ -1,11 +1,15 @@
package response
import "time"
import (
"encoding/json"
"time"
)
type WikiResponse struct {
ID string `json:"id"`
Title string `json:"title,omitempty"`
Content string `json:"content,omitempty"`
Content json.RawMessage `json:"content,omitempty"`
ProjectID string `json:"project_id"`
IsDeleted bool `json:"is_deleted,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
}
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
INSERT INTO entities (
name, description, thumbnail_url
id, name, slug, description, project_id, status
) 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 {
Name string `json:"name"`
Slug pgtype.Text `json:"slug"`
Description pgtype.Text `json:"description"`
ThumbnailUrl pgtype.Text `json:"thumbnail_url"`
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) {
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
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Slug,
&i.Description,
&i.ThumbnailUrl,
&i.Status,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
@@ -41,6 +53,17 @@ func (q *Queries) CreateEntity(ctx context.Context, arg CreateEntityParams) (Ent
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
UPDATE entities
SET
@@ -54,7 +77,7 @@ func (q *Queries) DeleteEntity(ctx context.Context, id pgtype.UUID) error {
}
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) {
@@ -68,9 +91,47 @@ func (q *Queries) GetEntitiesByIDs(ctx context.Context, dollar_1 []pgtype.UUID)
var i Entity
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Slug,
&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.CreatedAt,
&i.UpdatedAt,
@@ -86,7 +147,7 @@ func (q *Queries) GetEntitiesByIDs(ctx context.Context, dollar_1 []pgtype.UUID)
}
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
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
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Slug,
&i.Description,
&i.ThumbnailUrl,
&i.Status,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
@@ -107,23 +170,30 @@ func (q *Queries) GetEntityById(ctx context.Context, id pgtype.UUID) (Entity, er
}
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
WHERE is_deleted = false
AND name ILIKE '%' || $1::text || '%'
AND ($2::uuid IS NULL OR id < $2::uuid)
AND ($1::uuid IS NULL OR project_id = $1::uuid)
AND name ILIKE '%' || $2::text || '%'
AND ($3::uuid IS NULL OR id < $3::uuid)
ORDER BY id DESC
LIMIT $3
LIMIT $4
`
type SearchEntitiesParams struct {
ProjectID pgtype.UUID `json:"project_id"`
Name string `json:"name"`
CursorID pgtype.UUID `json:"cursor_id"`
LimitCount int32 `json:"limit_count"`
}
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 {
return nil, err
}
@@ -133,9 +203,11 @@ func (q *Queries) SearchEntities(ctx context.Context, arg SearchEntitiesParams)
var i Entity
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Slug,
&i.Description,
&i.ThumbnailUrl,
&i.Status,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,
@@ -154,32 +226,40 @@ const updateEntity = `-- name: UpdateEntity :one
UPDATE entities
SET
name = COALESCE($1, name),
description = COALESCE($2, description),
thumbnail_url = COALESCE($3, thumbnail_url)
WHERE id = $4 AND is_deleted = false
RETURNING id, name, description, thumbnail_url, is_deleted, created_at, updated_at
slug = COALESCE($2, slug),
description = COALESCE($3, description),
project_id = COALESCE($4, project_id),
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 {
Name pgtype.Text `json:"name"`
Slug pgtype.Text `json:"slug"`
Description pgtype.Text `json:"description"`
ThumbnailUrl pgtype.Text `json:"thumbnail_url"`
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) {
row := q.db.QueryRow(ctx, updateEntity,
arg.Name,
arg.Slug,
arg.Description,
arg.ThumbnailUrl,
arg.ProjectID,
arg.Status,
arg.ID,
)
var i Entity
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Name,
&i.Slug,
&i.Description,
&i.ThumbnailUrl,
&i.Status,
&i.IsDeleted,
&i.CreatedAt,
&i.UpdatedAt,

View File

@@ -38,30 +38,42 @@ func (q *Queries) BulkDeleteEntityGeometriesByEntityId(ctx context.Context, enti
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
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 {
EntityID pgtype.UUID `json:"entity_id"`
ProjectID pgtype.UUID `json:"project_id"`
GeometryIds []pgtype.UUID `json:"geometry_ids"`
}
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
}
const createGeometry = `-- name: CreateGeometry :one
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 (
$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,
is_deleted, created_at, updated_at
`
@@ -72,6 +84,8 @@ type CreateGeometryParams struct {
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"`
ProjectID pgtype.UUID `json:"project_id"`
ID pgtype.UUID `json:"id"`
MinLng float64 `json:"min_lng"`
MinLat float64 `json:"min_lat"`
MaxLng float64 `json:"max_lng"`
@@ -85,6 +99,7 @@ type CreateGeometryRow struct {
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"`
@@ -101,6 +116,8 @@ func (q *Queries) CreateGeometry(ctx context.Context, arg CreateGeometryParams)
arg.Binding,
arg.TimeStart,
arg.TimeEnd,
arg.ProjectID,
arg.ID,
arg.MinLng,
arg.MinLat,
arg.MaxLng,
@@ -114,6 +131,7 @@ func (q *Queries) CreateGeometry(ctx context.Context, arg CreateGeometryParams)
&i.Binding,
&i.TimeStart,
&i.TimeEnd,
&i.ProjectID,
&i.MinLng,
&i.MinLat,
&i.MaxLng,
@@ -125,6 +143,42 @@ func (q *Queries) CreateGeometry(ctx context.Context, arg CreateGeometryParams)
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
UPDATE geometries
SET
@@ -139,7 +193,7 @@ func (q *Queries) DeleteGeometry(ctx context.Context, id pgtype.UUID) error {
const getGeometriesByIDs = `-- name: GetGeometriesByIDs :many
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_YMin(bbox)::float8 as min_lat,
ST_XMax(bbox)::float8 as max_lng,
@@ -156,6 +210,7 @@ type GetGeometriesByIDsRow struct {
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"`
@@ -181,6 +236,71 @@ func (q *Queries) GetGeometriesByIDs(ctx context.Context, dollar_1 []pgtype.UUID
&i.Binding,
&i.TimeStart,
&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.MinLat,
&i.MaxLng,
@@ -200,7 +320,7 @@ func (q *Queries) GetGeometriesByIDs(ctx context.Context, dollar_1 []pgtype.UUID
}
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,
is_deleted, created_at, updated_at
FROM geometries
@@ -214,6 +334,7 @@ type GetGeometryByIdRow struct {
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"`
@@ -233,6 +354,7 @@ func (q *Queries) GetGeometryById(ctx context.Context, id pgtype.UUID) (GetGeome
&i.Binding,
&i.TimeStart,
&i.TimeEnd,
&i.ProjectID,
&i.MinLng,
&i.MinLat,
&i.MaxLng,
@@ -246,7 +368,7 @@ func (q *Queries) GetGeometryById(ctx context.Context, id pgtype.UUID) (GetGeome
const searchGeometries = `-- name: SearchGeometries :many
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_YMin(g.bbox)::float8 as min_lat,
ST_XMax(g.bbox)::float8 as max_lng,
@@ -254,36 +376,38 @@ SELECT
g.is_deleted, g.created_at, g.updated_at
FROM geometries g
WHERE g.is_deleted = false
AND ($1::uuid IS NULL OR g.project_id = $1::uuid)
AND (
$1::float8 IS NULL OR
$2::float8 IS NULL OR
$3::float8 IS NULL OR
$4::float8 IS NULL OR
$5::float8 IS NULL OR
g.bbox && ST_MakeEnvelope(
$1::float8,
$2::float8,
$3::float8,
$4::float8,
$5::float8,
4326
)
)
AND (
$5::int IS NULL OR
(g.time_start <= $5::int AND g.time_end >= $5::int)
$6::int IS NULL OR
(g.time_start <= $6::int AND g.time_end >= $6::int)
)
AND (
$6::uuid IS NULL OR
$7::uuid IS NULL OR
EXISTS (
SELECT 1
FROM entity_geometries eg
WHERE eg.geometry_id = g.id
AND eg.entity_id = $6::uuid
AND eg.entity_id = $7::uuid
)
)
ORDER BY g.id DESC
`
type SearchGeometriesParams struct {
ProjectID pgtype.UUID `json:"project_id"`
SearchMinLng pgtype.Float8 `json:"search_min_lng"`
SearchMinLat pgtype.Float8 `json:"search_min_lat"`
SearchMaxLng pgtype.Float8 `json:"search_max_lng"`
@@ -299,6 +423,7 @@ type SearchGeometriesRow struct {
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"`
@@ -310,6 +435,7 @@ type SearchGeometriesRow struct {
func (q *Queries) SearchGeometries(ctx context.Context, arg SearchGeometriesParams) ([]SearchGeometriesRow, error) {
rows, err := q.db.Query(ctx, searchGeometries,
arg.ProjectID,
arg.SearchMinLng,
arg.SearchMinLat,
arg.SearchMaxLng,
@@ -331,6 +457,7 @@ func (q *Queries) SearchGeometries(ctx context.Context, arg SearchGeometriesPara
&i.Binding,
&i.TimeStart,
&i.TimeEnd,
&i.ProjectID,
&i.MinLng,
&i.MinLat,
&i.MaxLng,
@@ -357,14 +484,15 @@ SET
binding = COALESCE($3, binding),
time_start = COALESCE($4, time_start),
time_end = COALESCE($5, time_end),
project_id = COALESCE($6, project_id),
bbox = CASE
WHEN $6::boolean = true THEN
ST_MakeEnvelope($7::float8, $8::float8, $9::float8, $10::float8, 4326)
WHEN $7::boolean = true THEN
ST_MakeEnvelope($8::float8, $9::float8, $10::float8, $11::float8, 4326)
ELSE bbox
END,
updated_at = now()
WHERE id = $11 AND is_deleted = false
RETURNING id, geo_type, draw_geometry, binding, time_start, time_end,
WHERE id = $12 AND is_deleted = false
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,
is_deleted, created_at, updated_at
`
@@ -375,6 +503,7 @@ type UpdateGeometryParams struct {
Binding []byte `json:"binding"`
TimeStart pgtype.Int4 `json:"time_start"`
TimeEnd pgtype.Int4 `json:"time_end"`
ProjectID pgtype.UUID `json:"project_id"`
UpdateBbox pgtype.Bool `json:"update_bbox"`
MinLng pgtype.Float8 `json:"min_lng"`
MinLat pgtype.Float8 `json:"min_lat"`
@@ -390,6 +519,7 @@ type UpdateGeometryRow struct {
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"`
@@ -406,6 +536,7 @@ func (q *Queries) UpdateGeometry(ctx context.Context, arg UpdateGeometryParams)
arg.Binding,
arg.TimeStart,
arg.TimeEnd,
arg.ProjectID,
arg.UpdateBbox,
arg.MinLng,
arg.MinLat,
@@ -421,6 +552,7 @@ func (q *Queries) UpdateGeometry(ctx context.Context, arg UpdateGeometryParams)
&i.Binding,
&i.TimeStart,
&i.TimeEnd,
&i.ProjectID,
&i.MinLng,
&i.MinLat,
&i.MaxLng,

View File

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

View File

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

View File

@@ -37,42 +37,62 @@ func (q *Queries) BulkDeleteEntityWikisByEntityId(ctx context.Context, entityID
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
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 {
EntityID pgtype.UUID `json:"entity_id"`
ProjectID pgtype.UUID `json:"project_id"`
WikiIds []pgtype.UUID `json:"wiki_ids"`
}
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
}
const createWiki = `-- name: CreateWiki :one
INSERT INTO wikis (
title, content
id, title, content, project_id
) 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 {
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) {
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
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Title,
&i.Content,
&i.IsDeleted,
@@ -82,6 +102,31 @@ func (q *Queries) CreateWiki(ctx context.Context, arg CreateWikiParams) (Wiki, e
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
UPDATE wikis
SET
@@ -94,8 +139,19 @@ func (q *Queries) DeleteWiki(ctx context.Context, id pgtype.UUID) error {
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
SELECT id, title, content, is_deleted, created_at, updated_at
SELECT id, project_id, title, content, is_deleted, created_at, updated_at
FROM wikis
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
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Title,
&i.Content,
&i.IsDeleted,
@@ -115,7 +172,7 @@ func (q *Queries) GetWikiById(ctx context.Context, id pgtype.UUID) (Wiki, error)
}
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) {
@@ -129,6 +186,41 @@ func (q *Queries) GetWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]
var i Wiki
if err := rows.Scan(
&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.Content,
&i.IsDeleted,
@@ -146,26 +238,28 @@ func (q *Queries) GetWikisByIDs(ctx context.Context, dollar_1 []pgtype.UUID) ([]
}
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
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 (
$2::uuid IS NULL OR
$3::uuid IS NULL OR
EXISTS (
SELECT 1
FROM entity_wikis ew
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
LIMIT $4
LIMIT $5
`
type SearchWikisParams struct {
ProjectID pgtype.UUID `json:"project_id"`
Title string `json:"title"`
EntityID pgtype.UUID `json:"entity_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) {
rows, err := q.db.Query(ctx, searchWikis,
arg.ProjectID,
arg.Title,
arg.EntityID,
arg.CursorID,
@@ -188,6 +283,7 @@ func (q *Queries) SearchWikis(ctx context.Context, arg SearchWikisParams) ([]Wik
var i Wiki
if err := rows.Scan(
&i.ID,
&i.ProjectID,
&i.Title,
&i.Content,
&i.IsDeleted,
@@ -208,22 +304,30 @@ const updateWiki = `-- name: UpdateWiki :one
UPDATE wikis
SET
title = COALESCE($1, title),
content = COALESCE($2, content)
WHERE id = $3 AND is_deleted = false
RETURNING id, title, content, is_deleted, created_at, updated_at
content = COALESCE($2, content),
project_id = COALESCE($3, project_id)
WHERE id = $4 AND is_deleted = false
RETURNING id, project_id, title, content, is_deleted, created_at, updated_at
`
type UpdateWikiParams struct {
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) 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
err := row.Scan(
&i.ID,
&i.ProjectID,
&i.Title,
&i.Content,
&i.IsDeleted,

View File

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

View File

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

View File

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

View File

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

View File

@@ -20,6 +20,7 @@ type CommitRepository interface {
GetByID(ctx context.Context, id 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)
UpdateSnapshot(ctx context.Context, id pgtype.UUID, snapshot json.RawMessage) (*models.CommitEntity, error)
WithTx(tx pgx.Tx) CommitRepository
}
@@ -228,3 +229,25 @@ func (r *commitRepository) Search(ctx context.Context, params sqlc.SearchCommits
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)
Update(ctx context.Context, params sqlc.UpdateEntityParams) (*models.EntityEntity, 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
}
@@ -83,8 +85,10 @@ func (r *entityRepository) getByIDsWithFallback(ctx context.Context, ids []strin
item := models.EntityEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
Slug: convert.TextToString(row.Slug),
Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl),
ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
@@ -136,8 +140,10 @@ func (r *entityRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models
entity = models.EntityEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
Slug: convert.TextToString(row.Slug),
Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl),
ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
@@ -166,8 +172,10 @@ func (r *entityRepository) Search(ctx context.Context, params sqlc.SearchEntitie
entity := &models.EntityEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
Slug: convert.TextToString(row.Slug),
Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl),
ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
@@ -180,6 +188,7 @@ func (r *entityRepository) Search(ctx context.Context, params sqlc.SearchEntitie
if len(entityToCache) > 0 {
_ = r.c.MSet(ctx, entityToCache, constants.NormalCacheDuration)
}
if len(ids) > 0 {
_ = 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{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
Slug: convert.TextToString(row.Slug),
Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl),
ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
go func() {
_ = r.c.DelByPattern(context.Background(), "entity:search*")
}()
return &entity, nil
}
@@ -216,8 +225,10 @@ func (r *entityRepository) Update(ctx context.Context, params sqlc.UpdateEntityP
entity := models.EntityEntity{
ID: convert.UUIDToString(row.ID),
Name: row.Name,
Slug: convert.TextToString(row.Slug),
Description: convert.TextToString(row.Description),
ThumbnailUrl: convert.TextToString(row.ThumbnailUrl),
ProjectID: convert.UUIDToString(row.ProjectID),
Status: convert.Int2ToInt16Ptr(row.Status),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
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)))
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
CreateEntityGeometries(ctx context.Context, params sqlc.CreateEntityGeometriesParams) 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
}
@@ -96,6 +101,7 @@ func (r *geometryRepository) getByIDsWithFallback(ctx context.Context, ids []str
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
ProjectID: convert.UUIDToString(row.ProjectID),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
@@ -157,6 +163,7 @@ func (r *geometryRepository) GetByID(ctx context.Context, id pgtype.UUID) (*mode
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
ProjectID: convert.UUIDToString(row.ProjectID),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
@@ -195,6 +202,7 @@ func (r *geometryRepository) Search(ctx context.Context, params sqlc.SearchGeome
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
ProjectID: convert.UUIDToString(row.ProjectID),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
@@ -233,15 +241,12 @@ func (r *geometryRepository) Create(ctx context.Context, params sqlc.CreateGeome
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
ProjectID: convert.UUIDToString(row.ProjectID),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
go func() {
_ = r.c.DelByPattern(context.Background(), "geometry:search*")
}()
return &geometry, nil
}
@@ -263,6 +268,7 @@ func (r *geometryRepository) Update(ctx context.Context, params sqlc.UpdateGeome
MaxLng: row.MaxLng,
MaxLat: row.MaxLat,
},
ProjectID: convert.UUIDToString(row.ProjectID),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
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 {
err := r.q.CreateEntityGeometries(ctx, params)
if err != nil {
return err
}
return err
return r.q.CreateEntityGeometries(ctx, params)
}
func (r *geometryRepository) DeleteEntityGeometriesByProjectID(ctx context.Context, projectID pgtype.UUID) error {
return r.q.DeleteEntityGeometriesByProjectID(ctx, projectID)
}
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 {
return err
}
if len(geometryIDs) > 0 {
keys := make([]string, len(geometryIDs))
for i, id := range geometryIDs {
return nil
}
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))
}
go func() {
_ = r.c.Del(context.Background(), keys...)
}()
_ = r.c.Del(ctx, keys...)
}
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),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
}
_ = item.ParseUser(row.User)
_ = item.ParseCommits(row.Commits)
_ = item.ParseSubmissions(row.Submissions)
_ = item.ParseMembers(row.Members)
dbMap[item.ID] = &item
}
@@ -158,10 +158,10 @@ func (r *projectRepository) GetByID(ctx context.Context, id pgtype.UUID) (*model
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
}
_ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits)
_ = project.ParseSubmissions(row.Submissions)
_ = project.ParseMembers(row.Members)
_ = 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),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
}
_ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits)
_ = project.ParseSubmissions(row.Submissions)
_ = project.ParseMembers(row.Members)
ids = append(ids, project.ID)
@@ -245,10 +245,10 @@ func (r *projectRepository) Search(ctx context.Context, params sqlc.SearchProjec
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
}
_ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits)
_ = project.ParseSubmissions(row.Submissions)
_ = project.ParseMembers(row.Members)
ids = append(ids, project.ID)
@@ -299,10 +299,10 @@ func (r *projectRepository) Create(ctx context.Context, params sqlc.CreateProjec
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
}
_ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits)
_ = project.ParseSubmissions(row.Submissions)
_ = project.ParseMembers(row.Members)
go func() {
@@ -330,10 +330,10 @@ func (r *projectRepository) Update(ctx context.Context, params sqlc.UpdateProjec
UserID: convert.UUIDToString(row.UserID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
SubmissionIds: convert.ListUUIDToString(row.SubmissionIds),
}
_ = project.ParseUser(row.User)
_ = project.ParseCommits(row.Commits)
_ = project.ParseSubmissions(row.Submissions)
_ = project.ParseMembers(row.Members)
_ = 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
CreateEntityWikis(ctx context.Context, params sqlc.CreateEntityWikisParams) 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
}
@@ -85,8 +90,9 @@ func (r *wikiRepository) getByIDsWithFallback(ctx context.Context, ids []string)
item := models.WikiEntity{
ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title),
Content: convert.TextToString(row.Content),
Content: json.RawMessage(row.Content),
IsDeleted: row.IsDeleted,
ProjectID: convert.UUIDToString(row.ProjectID),
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
@@ -137,7 +143,7 @@ func (r *wikiRepository) GetByID(ctx context.Context, id pgtype.UUID) (*models.W
wiki = models.WikiEntity{
ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title),
Content: convert.TextToString(row.Content),
Content: json.RawMessage(row.Content),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
@@ -166,7 +172,7 @@ func (r *wikiRepository) Search(ctx context.Context, params sqlc.SearchWikisPara
wiki := &models.WikiEntity{
ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title),
Content: convert.TextToString(row.Content),
Content: json.RawMessage(row.Content),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
@@ -195,16 +201,12 @@ func (r *wikiRepository) Create(ctx context.Context, params sqlc.CreateWikiParam
wiki := models.WikiEntity{
ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title),
Content: convert.TextToString(row.Content),
Content: json.RawMessage(row.Content),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
}
go func() {
_ = r.c.DelByPattern(context.Background(), "wiki:search*")
}()
return &wiki, nil
}
@@ -216,7 +218,7 @@ func (r *wikiRepository) Update(ctx context.Context, params sqlc.UpdateWikiParam
wiki := models.WikiEntity{
ID: convert.UUIDToString(row.ID),
Title: convert.TextToString(row.Title),
Content: convert.TextToString(row.Content),
Content: json.RawMessage(row.Content),
IsDeleted: row.IsDeleted,
CreatedAt: convert.TimeToPtr(row.CreatedAt),
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 {
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 {
return err
}
return nil
}
func (r *wikiRepository) BulkDeleteEntityWikisByEntityId(ctx context.Context, entityId pgtype.UUID) error {
wikiIDs, err := r.q.BulkDeleteEntityWikisByEntityId(ctx, entityId)
func (r *wikiRepository) GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.WikiEntity, error) {
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 {
return err
}
if len(wikiIDs) > 0 {
keys := make([]string, len(wikiIDs))
for i, id := range wikiIDs {
if len(ids) > 0 {
keys := make([]string, len(ids))
for i, id := range ids {
keys[i] = fmt.Sprintf("wiki:id:%s", convert.UUIDToString(id))
}
go func() {
_ = r.c.Del(context.Background(), keys...)
}()
_ = r.c.Del(ctx, keys...)
}
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 (
"context"
"encoding/json"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
"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")
}
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
}
@@ -90,9 +97,14 @@ func (s *commitService) CreateCommit(ctx context.Context, userID string, project
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{
ProjectID: projectUUID,
SnapshotJson: dto.SnapshotJson,
SnapshotJson: snapshotJSON,
UserID: userUUID,
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
}
if req.ProjectID != nil {
projectID, err := convert.StringToUUID(*req.ProjectID)
if err == nil {
params.ProjectID = projectID
}
}
entities, err := s.entityRepo.Search(ctx, params)
if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to search entities")

View File

@@ -2,6 +2,7 @@ package services
import (
"context"
"encoding/json"
"fmt"
"history-api/internal/dtos/request"
"history-api/internal/dtos/response"
@@ -32,6 +33,9 @@ type submissionService struct {
projectRepo repositories.ProjectRepository
commitRepo repositories.CommitRepository
userRepo repositories.UserRepository
wikiRepo repositories.WikiRepository
geometryRepo repositories.GeometryRepository
entityRepo repositories.EntityRepository
db *pgxpool.Pool
c cache.Cache
}
@@ -41,6 +45,9 @@ func NewSubmissionService(
projectRepo repositories.ProjectRepository,
commitRepo repositories.CommitRepository,
userRepo repositories.UserRepository,
wikiRepo repositories.WikiRepository,
geometryRepo repositories.GeometryRepository,
entityRepo repositories.EntityRepository,
db *pgxpool.Pool,
c cache.Cache,
) SubmissionService {
@@ -49,6 +56,9 @@ func NewSubmissionService(
projectRepo: projectRepo,
commitRepo: commitRepo,
userRepo: userRepo,
wikiRepo: wikiRepo,
geometryRepo: geometryRepo,
entityRepo: entityRepo,
db: db,
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")
}
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{
ProjectID: projectUUID,
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) {
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)
if err != nil {
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")
}
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{
ID: submissionUUID,
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),
}
updatedSubmission, err := s.submissionRepo.Update(ctx, arg)
updatedSubmission, err := submissionRepo.Update(ctx, arg)
if err != nil {
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))
return updatedSubmission.ToResponse(), nil

View File

@@ -58,6 +58,7 @@ func (s *wikiService) SearchWikis(ctx context.Context, req *request.SearchWikiDt
if req.Title != "" {
params.Title = req.Title
}
if req.EntityID != "" {
entityId, err := convert.StringToUUID(req.EntityID)
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)
if err != nil {
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
}
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 {
if !v.Valid {
return nil
@@ -114,3 +124,31 @@ func Int4ToInt32(v pgtype.Int4) int32 {
}
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,
}
}