From bcc2e192c1914671dbe59de29598f255c054ee74 Mon Sep 17 00:00:00 2001 From: AzenKain Date: Mon, 4 May 2026 09:55:17 +0700 Subject: [PATCH] UPDATE: Submission module --- cmd/api/server.go | 5 +- commit_snapshot.md | 355 ++++++++++++++ ...oject.down.sql => 000006_project.down.sql} | 0 ...9_project.up.sql => 000006_project.up.sql} | 5 +- ...ties.down.sql => 000007_entities.down.sql} | 0 ...entities.up.sql => 000007_entities.up.sql} | 8 +- ...007_wiki.down.sql => 000008_wiki.down.sql} | 0 ...{000007_wiki.up.sql => 000008_wiki.up.sql} | 8 +- ...es.down.sql => 000009_geometries.down.sql} | 0 ...etries.up.sql => 000009_geometries.up.sql} | 12 +- db/query/commit.sql | 6 + db/query/entities.sql | 19 +- db/query/geometries.sql | 49 +- db/query/project.sql | 30 +- db/query/wiki.sql | 36 +- db/schema.sql | 99 ++-- docs/docs.go | 373 ++++++++++++++- docs/swagger.json | 373 ++++++++++++++- docs/swagger.yaml | 254 +++++++++- internal/dtos/request/commit.go | 4 +- internal/dtos/request/entity.go | 7 +- internal/dtos/request/geometry.go | 1 + internal/dtos/request/snapshot.go | 105 ++-- internal/dtos/request/wiki.go | 9 +- internal/dtos/response/entity.go | 10 +- internal/dtos/response/geometry.go | 1 + internal/dtos/response/project.go | 35 +- internal/dtos/response/wiki.go | 18 +- internal/gen/sqlc/commit.sql.go | 28 ++ internal/gen/sqlc/entities.sql.go | 136 ++++-- internal/gen/sqlc/geometries.sql.go | 170 ++++++- internal/gen/sqlc/models.go | 26 +- internal/gen/sqlc/project.sql.go | 54 ++- internal/gen/sqlc/wiki.sql.go | 156 +++++- internal/models/entity.go | 8 +- internal/models/geometry.go | 2 + internal/models/project.go | 25 +- internal/models/wiki.go | 15 +- internal/repositories/commitRepository.go | 23 + internal/repositories/entityRepository.go | 85 +++- internal/repositories/geometryRepository.go | 105 +++- internal/repositories/projectRepository.go | 12 +- internal/repositories/wikiRepository.go | 96 +++- internal/services/commitService.go | 14 +- internal/services/entityService.go | 7 + internal/services/submissionService.go | 447 +++++++++++++++++- internal/services/wikiService.go | 8 + pkg/convert/convert.go | 38 ++ 48 files changed, 2918 insertions(+), 359 deletions(-) create mode 100644 commit_snapshot.md rename db/migrations/{000009_project.down.sql => 000006_project.down.sql} (100%) rename db/migrations/{000009_project.up.sql => 000006_project.up.sql} (94%) rename db/migrations/{000006_entities.down.sql => 000007_entities.down.sql} (100%) rename db/migrations/{000006_entities.up.sql => 000007_entities.up.sql} (76%) rename db/migrations/{000007_wiki.down.sql => 000008_wiki.down.sql} (100%) rename db/migrations/{000007_wiki.up.sql => 000008_wiki.up.sql} (74%) rename db/migrations/{000008_geometries.down.sql => 000009_geometries.down.sql} (100%) rename db/migrations/{000008_geometries.up.sql => 000009_geometries.up.sql} (82%) diff --git a/cmd/api/server.go b/cmd/api/server.go index 861ade8..faf3646 100644 --- a/cmd/api/server.go +++ b/cmd/api/server.go @@ -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) diff --git a/commit_snapshot.md b/commit_snapshot.md new file mode 100644 index 0000000..ce7a91d --- /dev/null +++ b/commit_snapshot.md @@ -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"`. diff --git a/db/migrations/000009_project.down.sql b/db/migrations/000006_project.down.sql similarity index 100% rename from db/migrations/000009_project.down.sql rename to db/migrations/000006_project.down.sql diff --git a/db/migrations/000009_project.up.sql b/db/migrations/000006_project.up.sql similarity index 94% rename from db/migrations/000009_project.up.sql rename to db/migrations/000006_project.up.sql index 6cc0f1b..2945747 100644 --- a/db/migrations/000009_project.up.sql +++ b/db/migrations/000006_project.up.sql @@ -22,4 +22,7 @@ CREATE INDEX idx_projects_status_updated ON projects (project_status, updated_at DESC); CREATE INDEX idx_projects_title_trgm -ON projects USING GIN (title gin_trgm_ops); \ No newline at end of file +ON projects USING GIN (title gin_trgm_ops); + + + diff --git a/db/migrations/000006_entities.down.sql b/db/migrations/000007_entities.down.sql similarity index 100% rename from db/migrations/000006_entities.down.sql rename to db/migrations/000007_entities.down.sql diff --git a/db/migrations/000006_entities.up.sql b/db/migrations/000007_entities.up.sql similarity index 76% rename from db/migrations/000006_entities.up.sql rename to db/migrations/000007_entities.up.sql index 9c0bbff..1ab567e 100644 --- a/db/migrations/000006_entities.up.sql +++ b/db/migrations/000007_entities.up.sql @@ -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; diff --git a/db/migrations/000007_wiki.down.sql b/db/migrations/000008_wiki.down.sql similarity index 100% rename from db/migrations/000007_wiki.down.sql rename to db/migrations/000008_wiki.down.sql diff --git a/db/migrations/000007_wiki.up.sql b/db/migrations/000008_wiki.up.sql similarity index 74% rename from db/migrations/000007_wiki.up.sql rename to db/migrations/000008_wiki.up.sql index bc57885..8ddab87 100644 --- a/db/migrations/000007_wiki.up.sql +++ b/db/migrations/000008_wiki.up.sql @@ -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 diff --git a/db/migrations/000008_geometries.down.sql b/db/migrations/000009_geometries.down.sql similarity index 100% rename from db/migrations/000008_geometries.down.sql rename to db/migrations/000009_geometries.down.sql diff --git a/db/migrations/000008_geometries.up.sql b/db/migrations/000009_geometries.up.sql similarity index 82% rename from db/migrations/000008_geometries.up.sql rename to db/migrations/000009_geometries.up.sql index 67edc72..6ad4a01 100644 --- a/db/migrations/000008_geometries.up.sql +++ b/db/migrations/000009_geometries.up.sql @@ -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 diff --git a/db/query/commit.sql b/db/query/commit.sql index befac10..16b47ba 100644 --- a/db/query/commit.sql +++ b/db/query/commit.sql @@ -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 *; diff --git a/db/query/entities.sql b/db/query/entities.sql index 89e1ec4..4100e72 100644 --- a/db/query/entities.sql +++ b/db/query/entities.sql @@ -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[]); diff --git a/db/query/geometries.sql b/db/query/geometries.sql index f41f8be..e931bd7 100644 --- a/db/query/geometries.sql +++ b/db/query/geometries.sql @@ -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; diff --git a/db/query/project.sql b/db/query/project.sql index 59d5a93..71d5652 100644 --- a/db/query/project.sql +++ b/db/query/project.sql @@ -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, diff --git a/db/query/wiki.sql b/db/query/wiki.sql index 3f99722..990c302 100644 --- a/db/query/wiki.sql +++ b/db/query/wiki.sql @@ -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; diff --git a/db/schema.sql b/db/schema.sql index 3837f8d..5946e96 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -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, diff --git a/docs/docs.go b/docs/docs.go index f081d3e..d8820d9 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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": { diff --git a/docs/swagger.json b/docs/swagger.json index 66d09ff..a9759ee 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e13c4b8..576fa4e 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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 diff --git a/internal/dtos/request/commit.go b/internal/dtos/request/commit.go index 3537a0d..b044837 100644 --- a/internal/dtos/request/commit.go +++ b/internal/dtos/request/commit.go @@ -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"` } diff --git a/internal/dtos/request/entity.go b/internal/dtos/request/entity.go index 8cc52ee..0d20ae1 100644 --- a/internal/dtos/request/entity.go +++ b/internal/dtos/request/entity.go @@ -1,7 +1,8 @@ package request 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"` + 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"` } diff --git a/internal/dtos/request/geometry.go b/internal/dtos/request/geometry.go index 59e01c5..d14845e 100644 --- a/internal/dtos/request/geometry.go +++ b/internal/dtos/request/geometry.go @@ -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"` } diff --git a/internal/dtos/request/snapshot.go b/internal/dtos/request/snapshot.go index bfe45df..7371447 100644 --- a/internal/dtos/request/snapshot.go +++ b/internal/dtos/request/snapshot.go @@ -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"` + 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"` + 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"` + Type string `json:"type" validate:"required,eq=FeatureCollection"` + Features []*Feature `json:"features" validate:"required,dive"` } type Feature struct { - Type string `json:"type" validate:"required,eq=Feature"` - Properties FeatureProperties `json:"properties" validate:"required"` - Geometry json.RawMessage `json:"geometry" validate:"required"` + Type string `json:"type" validate:"required,eq=Feature"` + 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"` - 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"` - EntityName string `json:"entity_name,omitempty"` - EntityNames []string `json:"entity_names,omitempty"` - EntityTypeID string `json:"entity_type_id,omitempty"` + 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" 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" validate:"omitempty,uuidv7"` } type EntitySnapshot struct { - ID string `json:"id" validate:"required"` - 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"` - 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"` + ID string `json:"id" validate:"required,uuidv7"` + Source string `json:"source,omitempty" validate:"omitempty,oneof=inline ref"` + Operation string `json:"operation,omitempty" validate:"omitempty,oneof=create update delete reference"` + Name string `json:"name,omitempty"` + Slug *string `json:"slug,omitempty"` + Description string `json:"description,omitempty"` + TypeID string `json:"type_id,omitempty"` + Status *int `json:"status,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 - BaseLinksHash string `json:"base_links_hash,omitempty"` +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"` - IsDeleted *int `json:"is_deleted,omitempty" validate:"omitempty,oneof=0 1"` -} -type Ref struct { - ID string `json:"id" validate:"required"` -} \ No newline at end of file + // Legacy / Compatibility + IsDeleted *int `json:"is_deleted,omitempty" validate:"omitempty,oneof=0 1"` +} diff --git a/internal/dtos/request/wiki.go b/internal/dtos/request/wiki.go index 42efd85..5ec55ed 100644 --- a/internal/dtos/request/wiki.go +++ b/internal/dtos/request/wiki.go @@ -1,8 +1,9 @@ package request type SearchWikiDto struct { - Cursor string `json:"cursor" query:"cursor" 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"` + 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"` } diff --git a/internal/dtos/response/entity.go b/internal/dtos/response/entity.go index f43c745..e477171 100644 --- a/internal/dtos/response/entity.go +++ b/internal/dtos/response/entity.go @@ -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"` } diff --git a/internal/dtos/response/geometry.go b/internal/dtos/response/geometry.go index 5841d04..c640147 100644 --- a/internal/dtos/response/geometry.go +++ b/internal/dtos/response/geometry.go @@ -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"` diff --git a/internal/dtos/response/project.go b/internal/dtos/response/project.go index 8ef4320..999f724 100644 --- a/internal/dtos/response/project.go +++ b/internal/dtos/response/project.go @@ -14,19 +14,24 @@ type MemberSimpleResponse struct { AvatarUrl string `json:"avatar_url"` } -type ProjectResponse struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - LatestCommitID *string `json:"latest_commit_id,omitempty"` - ProjectStatus string `json:"project_status"` - LockedBy *string `json:"locked_by,omitempty"` - IsDeleted bool `json:"is_deleted"` - UserID string `json:"user_id"` - CreatedAt *time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at"` - User *UserSimpleResponse `json:"user,omitempty"` - Commits []CommitSimpleResponse `json:"commits"` - SubmissionIds []string `json:"submission_ids"` - Members []MemberSimpleResponse `json:"members"` +type SubmissionSimpleResponse struct { + ID string `json:"id"` + Status string `json:"status"` +} + +type ProjectResponse struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + LatestCommitID *string `json:"latest_commit_id,omitempty"` + ProjectStatus string `json:"project_status"` + LockedBy *string `json:"locked_by,omitempty"` + IsDeleted bool `json:"is_deleted"` + UserID string `json:"user_id"` + CreatedAt *time.Time `json:"created_at"` + UpdatedAt *time.Time `json:"updated_at"` + User *UserSimpleResponse `json:"user,omitempty"` + Commits []CommitSimpleResponse `json:"commits"` + Submissions []SubmissionSimpleResponse `json:"submissions"` + Members []MemberSimpleResponse `json:"members"` } diff --git a/internal/dtos/response/wiki.go b/internal/dtos/response/wiki.go index ddd2019..f6dad46 100644 --- a/internal/dtos/response/wiki.go +++ b/internal/dtos/response/wiki.go @@ -1,12 +1,16 @@ 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"` - IsDeleted bool `json:"is_deleted,omitempty"` - CreatedAt *time.Time `json:"created_at,omitempty"` - UpdatedAt *time.Time `json:"updated_at,omitempty"` + ID string `json:"id"` + Title string `json:"title,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"` } diff --git a/internal/gen/sqlc/commit.sql.go b/internal/gen/sqlc/commit.sql.go index 90a3871..a93d05e 100644 --- a/internal/gen/sqlc/commit.sql.go +++ b/internal/gen/sqlc/commit.sql.go @@ -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 +} diff --git a/internal/gen/sqlc/entities.sql.go b/internal/gen/sqlc/entities.sql.go index 3f7ae5c..c7e286e 100644 --- a/internal/gen/sqlc/entities.sql.go +++ b/internal/gen/sqlc/entities.sql.go @@ -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"` - Description pgtype.Text `json:"description"` - ThumbnailUrl pgtype.Text `json:"thumbnail_url"` + Name string `json:"name"` + Slug pgtype.Text `json:"slug"` + Description pgtype.Text `json:"description"` + ProjectID pgtype.UUID `json:"project_id"` + Status pgtype.Int2 `json:"status"` + ID pgtype.UUID `json:"id"` } func (q *Queries) CreateEntity(ctx context.Context, arg CreateEntityParams) (Entity, error) { - 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"` - Description pgtype.Text `json:"description"` - ThumbnailUrl pgtype.Text `json:"thumbnail_url"` - ID pgtype.UUID `json:"id"` + Name pgtype.Text `json:"name"` + Slug pgtype.Text `json:"slug"` + Description pgtype.Text `json:"description"` + 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, diff --git a/internal/gen/sqlc/geometries.sql.go b/internal/gen/sqlc/geometries.sql.go index b99b767..f830761 100644 --- a/internal/gen/sqlc/geometries.sql.go +++ b/internal/gen/sqlc/geometries.sql.go @@ -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, diff --git a/internal/gen/sqlc/models.go b/internal/gen/sqlc/models.go index 2069360..cb9e31a 100644 --- a/internal/gen/sqlc/models.go +++ b/internal/gen/sqlc/models.go @@ -22,23 +22,27 @@ type Commit struct { } type Entity struct { - ID pgtype.UUID `json:"id"` - Name string `json:"name"` - Description pgtype.Text `json:"description"` - ThumbnailUrl pgtype.Text `json:"thumbnail_url"` - IsDeleted bool `json:"is_deleted"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` + 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"` + Status pgtype.Int2 `json:"status"` + IsDeleted bool `json:"is_deleted"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` } 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"` + 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"` diff --git a/internal/gen/sqlc/project.sql.go b/internal/gen/sqlc/project.sql.go index 02b4548..a852099 100644 --- a/internal/gen/sqlc/project.sql.go +++ b/internal/gen/sqlc/project.sql.go @@ -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 diff --git a/internal/gen/sqlc/wiki.sql.go b/internal/gen/sqlc/wiki.sql.go index a7aee32..f4a8241 100644 --- a/internal/gen/sqlc/wiki.sql.go +++ b/internal/gen/sqlc/wiki.sql.go @@ -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"` - WikiIds []pgtype.UUID `json:"wiki_ids"` + 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"` + Title pgtype.Text `json:"title"` + 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"` - ID pgtype.UUID `json:"id"` + Title pgtype.Text `json:"title"` + 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, diff --git a/internal/models/entity.go b/internal/models/entity.go index 3fa7887..3c46023 100644 --- a/internal/models/entity.go +++ b/internal/models/entity.go @@ -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, diff --git a/internal/models/geometry.go b/internal/models/geometry.go index ece95d0..8f9c9d5 100644 --- a/internal/models/geometry.go +++ b/internal/models/geometry.go @@ -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, diff --git a/internal/models/project.go b/internal/models/project.go index 43c78fd..e9d3382 100644 --- a/internal/models/project.go +++ b/internal/models/project.go @@ -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, } } diff --git a/internal/models/wiki.go b/internal/models/wiki.go index 6ccf164..e023fe5 100644 --- a/internal/models/wiki.go +++ b/internal/models/wiki.go @@ -1,17 +1,19 @@ package models import ( + "encoding/json" "history-api/internal/dtos/response" "time" ) type WikiEntity struct { - ID string `json:"id"` - Title string `json:"title"` - Content string `json:"content"` - IsDeleted bool `json:"is_deleted"` - CreatedAt *time.Time `json:"created_at"` - UpdatedAt *time.Time `json:"updated_at"` + ID string `json:"id"` + Title string `json:"title"` + 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"` } func (w *WikiEntity) ToResponse() *response.WikiResponse { @@ -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, diff --git a/internal/repositories/commitRepository.go b/internal/repositories/commitRepository.go index dc2fa22..bc0f6d1 100644 --- a/internal/repositories/commitRepository.go +++ b/internal/repositories/commitRepository.go @@ -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 +} diff --git a/internal/repositories/entityRepository.go b/internal/repositories/entityRepository.go index 560fecd..c4fa387 100644 --- a/internal/repositories/entityRepository.go +++ b/internal/repositories/entityRepository.go @@ -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 +} diff --git a/internal/repositories/geometryRepository.go b/internal/repositories/geometryRepository.go index cabdf07..75315a0 100644 --- a/internal/repositories/geometryRepository.go +++ b/internal/repositories/geometryRepository.go @@ -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, + }) +} diff --git a/internal/repositories/projectRepository.go b/internal/repositories/projectRepository.go index 8f36882..18b688d 100644 --- a/internal/repositories/projectRepository.go +++ b/internal/repositories/projectRepository.go @@ -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)) diff --git a/internal/repositories/wikiRepository.go b/internal/repositories/wikiRepository.go index 290d7c3..81d2ee0 100644 --- a/internal/repositories/wikiRepository.go +++ b/internal/repositories/wikiRepository.go @@ -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, + }) +} diff --git a/internal/services/commitService.go b/internal/services/commitService.go index a745ca3..180ea20 100644 --- a/internal/services/commitService.go +++ b/internal/services/commitService.go @@ -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), }) diff --git a/internal/services/entityService.go b/internal/services/entityService.go index 0878314..1f84a7e 100644 --- a/internal/services/entityService.go +++ b/internal/services/entityService.go @@ -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") diff --git a/internal/services/submissionService.go b/internal/services/submissionService.go index a94e3c0..27b86bc 100644 --- a/internal/services/submissionService.go +++ b/internal/services/submissionService.go @@ -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 diff --git a/internal/services/wikiService.go b/internal/services/wikiService.go index bd19d89..69dc48c 100644 --- a/internal/services/wikiService.go +++ b/internal/services/wikiService.go @@ -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") diff --git a/pkg/convert/convert.go b/pkg/convert/convert.go index c4f92e7..c6f3374 100644 --- a/pkg/convert/convert.go +++ b/pkg/convert/convert.go @@ -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, + } +}