diff --git a/.changeset/empty-cooks-promise.md b/.changeset/empty-cooks-promise.md new file mode 100644 index 000000000..9b97050ae --- /dev/null +++ b/.changeset/empty-cooks-promise.md @@ -0,0 +1,5 @@ +--- +"geohub": patch +--- + +refactor: migrate SQL queries to use drizzle ORM in all endpoints. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0fca7d3bf..12bb2a20f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -137,6 +137,52 @@ Add the same issue number in the PR description. Example: You have made your contribution to the GeoHub project. +## Database connection + +### download schema from remote database (only first time) + +[drizzle ORM](https://orm.drizzle.team/docs/get-started-postgresql#node-postgres) is used to manage data connections in GeoHub. + +To pull existing database settings, the following command is used. + +```shell +pnpm drizzle-kit introspect +``` + +note. before running introspect, please make sure your `DATABASE_CONNECTION` variable in `.env` is pointing remote database connection. + +This will create all migration files under `sites/geohub/drizzle` folder. `schema.ts` should be copied to `src/lib/server/schema.ts` since it is used for sveltekit. + +### migrate database schema to remote + +If database schema needs to be changed, update `src/lib/server/schema.ts` first. + +Then, run the following commands to migrate and push changes to database. + +```shell +pnpm drizzle-kit:generate # generate migration patch files +pnpm drizzle-kit:migrate # push changes to database +``` + +note. before running introspect, please make sure your `DATABASE_CONNECTION` variable in `.env` is pointing the correct database connection. + +If you want to migrate database schema to docker, `DATABASE_CONNECTION` can be `postgres://docker:docker@localhost:25432/geodata?sslmode=disable` + +### In sveltekit + +`$lib/server/db` create drizzle instance for sveltekit. You can import `db` from `$lib/server/db` to connect to the database. + +```ts +import { eq } from "drizzle-orm"; +import { userSettingsInGeohub } from "$lib/server/schema"; +import { db } from "$lib/server/db"; + +const db = await getClient(); +const settings = await db.query.userSettingsInGeohub.findFirst({ + where: eq(userSettingsInGeohub.userEmail, user_email), +}); +``` + ## Folder strucuture of GeoHub This GeoHub repo is using monorepo structure to manage several Javascript packages and Python documentation. The main repository is located at `/sites/geohub` folder, and the source code is under `src`. The structure of `src` folder is as follows. diff --git a/backends/database/geohub-database.sql b/backends/database/geohub-database.sql deleted file mode 100644 index 0356f9ee5..000000000 --- a/backends/database/geohub-database.sql +++ /dev/null @@ -1,481 +0,0 @@ -CREATE SCHEMA IF NOT EXISTS geohub; - -CREATE TABLE IF NOT EXISTS geohub.dataset -( - id character varying NOT NULL, - url character varying NOT NULL, - name character varying NOT NULL, - description character varying , - is_raster boolean NOT NULL, - license character varying , - bounds geometry (Polygon, 4326) NOT NULL, - access_level integer NOT NULL DEFAULT 3, - createdat timestamp with time zone NOT NULL, - created_user character varying(100) NOT NULL, - updatedat timestamp with time zone, - updated_user character varying(100) , - PRIMARY KEY (id) -); - -COMMENT ON TABLE geohub.dataset IS 'this table manages metadata of files'; - -COMMENT ON COLUMN geohub.dataset.id IS 'md5 hash generated from URL'; - -COMMENT ON COLUMN geohub.dataset.url IS 'stores URL for dataset. e.g., URL for azure blob, URL for mosaicjson'; - -COMMENT ON COLUMN geohub.dataset.is_raster IS 'raster or vector'; - -COMMENT ON COLUMN geohub.dataset.license IS 'data license'; - -COMMENT ON COLUMN geohub.dataset.bounds IS 'bounds of data'; - -COMMENT ON COLUMN geohub.dataset.access_level IS '1: login user, 2: UNDP, 3: public'; - -CREATE INDEX IF NOT EXISTS dataset_bounds_geom_idx - ON geohub.dataset USING gist - (bounds); - -CREATE TABLE IF NOT EXISTS geohub.dataset_defaultstyle -( - dataset_id character varying NOT NULL, - layer_id character varying NOT NULL, - layer_type character varying NOT NULL, - source json NOT NULL, - style json NOT NULL, - colormap_name character varying , - classification_method character varying , - classification_method_2 character varying , - created_user character varying(100) NOT NULL, - createdat timestamp with time zone NOT NULL DEFAULT now(), - updatedat timestamp with time zone, - updated_user character varying(100) , - CONSTRAINT dataset_defaultstyle_pkey PRIMARY KEY (dataset_id, layer_id, layer_type), - CONSTRAINT "FK_dataset_TO_dataset_id, layer_type" FOREIGN KEY (dataset_id) - REFERENCES geohub.dataset (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE CASCADE - NOT VALID -); - -COMMENT ON TABLE geohub.dataset_defaultstyle IS 'This table is to manage the default layer style for each dataset'; -COMMENT ON COLUMN geohub.dataset_defaultstyle.dataset_id IS 'md5 hash generated from URL'; -COMMENT ON COLUMN geohub.dataset_defaultstyle.layer_id IS 'Layer ID. Band name if it is raster, layer ID if it is vector.'; -COMMENT ON COLUMN geohub.dataset_defaultstyle.layer_type IS 'fill, symbol, line, circle, heatmap, raster'; -COMMENT ON COLUMN geohub.dataset_defaultstyle.source IS 'JSON object for maplibre source'; -COMMENT ON COLUMN geohub.dataset_defaultstyle.style IS 'JSON object for maplibre layer style'; -COMMENT ON COLUMN geohub.dataset_defaultstyle.colormap_name IS 'colormap name if it is used'; -COMMENT ON COLUMN geohub.dataset_defaultstyle.classification_method IS 'classification method if it is used'; -COMMENT ON COLUMN geohub.dataset_defaultstyle.classification_method_2 IS 'classification method if there are two classification settings (icon size, line width) apart from color.'; - - -CREATE TABLE IF NOT EXISTS geohub.style -( - id serial NOT NULL, - name character varying NOT NULL, - style json NOT NULL, - createdat timestamp with time zone NOT NULL DEFAULT now(), - updatedat timestamp with time zone NOT NULL DEFAULT now(), - layers json, - access_level integer NOT NULL DEFAULT 1, - created_user character varying(100) , - updated_user character varying(100) , - PRIMARY KEY (id) -); - -COMMENT ON TABLE geohub.style IS 'this table manages style.json created at geohub'; - -CREATE TABLE IF NOT EXISTS geohub.style_favourite -( - style_id integer NOT NULL, - user_email character varying(100) NOT NULL, - savedat timestamp with time zone NOT NULL DEFAULT now(), - CONSTRAINT style_favourite_pkey PRIMARY KEY (style_id, user_email), - CONSTRAINT "FK_style_TO_style_favourite" FOREIGN KEY (style_id) - REFERENCES geohub.style (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE CASCADE - NOT VALID -); - -COMMENT ON TABLE geohub.style_favourite IS 'this table is to manage users favourite styles'; - -COMMENT ON COLUMN geohub.style_favourite.style_id IS 'Style ID'; - -COMMENT ON COLUMN geohub.style_favourite.user_email IS 'user email address'; - -COMMENT ON COLUMN geohub.style_favourite.savedat IS 'saved datetime'; - - -CREATE TABLE IF NOT EXISTS geohub.tag -( - id serial NOT NULL, - value character varying NOT NULL, - key character varying NOT NULL, - PRIMARY KEY (id) -); - -COMMENT ON TABLE geohub.tag IS 'this table manages tags'; - -COMMENT ON COLUMN geohub.tag.id IS 'unique ID for tag name'; - -COMMENT ON COLUMN geohub.tag.value IS 'tag value'; - -COMMENT ON COLUMN geohub.tag.key IS 'tag key'; - -CREATE TABLE IF NOT EXISTS geohub.dataset_tag -( - dataset_id character varying NOT NULL, - tag_id serial NOT NULL, - CONSTRAINT dataset_tag_pkey PRIMARY KEY (dataset_id, tag_id) -); - -COMMENT ON TABLE geohub.dataset_tag IS 'this table connects file_metadata and tag tables'; - -COMMENT ON COLUMN geohub.dataset_tag.dataset_id IS 'unique ID for dataset'; - -COMMENT ON COLUMN geohub.dataset_tag.tag_id IS 'unique ID for tag name'; - -ALTER TABLE geohub.dataset_tag - ADD CONSTRAINT FK_tag_TO_dataset_tag - FOREIGN KEY (tag_id) - REFERENCES geohub.tag (id); - -ALTER TABLE geohub.dataset_tag - ADD CONSTRAINT FK_dataset_TO_dataset_tag - FOREIGN KEY (dataset_id) - REFERENCES geohub.dataset (id); - --- DROP INDEX IF EXISTS geohub.tag_idx_key_value; - -CREATE INDEX IF NOT EXISTS tag_idx_key_value - ON geohub.tag USING btree - (key COLLATE pg_catalog."default" ASC NULLS LAST, value COLLATE pg_catalog."default" ASC NULLS LAST) - TABLESPACE pg_default; --- Index: tag_idx_value - --- DROP INDEX IF EXISTS geohub.tag_idx_value; - -CREATE INDEX IF NOT EXISTS tag_idx_value - ON geohub.tag USING btree - (value COLLATE pg_catalog."default" ASC NULLS LAST) - TABLESPACE pg_default; - -CREATE TABLE IF NOT EXISTS geohub.dataset_favourite -( - dataset_id character varying NOT NULL, - user_email character varying(100) NOT NULL, - savedat timestamp with time zone NOT NULL, - CONSTRAINT dataset_favourite_pkey PRIMARY KEY (dataset_id, user_email), - CONSTRAINT "FK_dataset_TO_dataset_favourite" FOREIGN KEY (dataset_id) - REFERENCES geohub.dataset (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION - NOT VALID -); - -COMMENT ON TABLE geohub.dataset_favourite - IS 'This table manages users to save their favourite datasets'; - -COMMENT ON COLUMN geohub.dataset_favourite.dataset_id - IS 'md5 hash generated from URL'; - -COMMENT ON COLUMN geohub.dataset_favourite.user_email - IS 'user email address'; - -COMMENT ON COLUMN geohub.dataset_favourite.savedat - IS 'timestamp which users saved'; - -CREATE TABLE IF NOT EXISTS geohub.country -( - iso_3 character varying NOT NULL, - iso_code integer NOT NULL, - iso_2 character varying NULL, - name character varying NOT NULL, - region1_code integer NOT NULL, - region1_name character varying NOT NULL, - region2_code integer NOT NULL, - region2_name character varying NOT NULL, - region3_code integer NOT NULL, - region3_name character varying NOT NULL, - CONSTRAINT country_pkey PRIMARY KEY (iso_3) -); - --- superuser table -CREATE TABLE IF NOT EXISTS geohub.superuser -( - user_email character varying(100) NOT NULL, - createdat timestamp with time zone NOT NULL DEFAULT now(), - CONSTRAINT superuser_pkey PRIMARY KEY (user_email) -); - --- ALTER TABLE IF EXISTS geohub.superuser --- OWNER to undpgeohub; - -COMMENT ON TABLE geohub.superuser - IS 'this table manages superusers across geohub app'; - --- dataset_permission table -CREATE TABLE IF NOT EXISTS geohub.dataset_permission -( - dataset_id character varying NOT NULL, - user_email character varying(100) NOT NULL, - permission smallint NOT NULL DEFAULT 1, - createdat timestamp with time zone NOT NULL DEFAULT now(), - updatedat timestamp with time zone, - CONSTRAINT dataset_permission_pkey PRIMARY KEY (dataset_id, user_email), - CONSTRAINT "FK_dataset_TO_dataset_permission" FOREIGN KEY (dataset_id) - REFERENCES geohub.dataset (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE CASCADE - NOT VALID -); - --- ALTER TABLE IF EXISTS geohub.dataset_permission --- OWNER to undpgeohub; - -COMMENT ON TABLE geohub.dataset_permission - IS 'this table manages users'' permission for operating each dataset'; - -COMMENT ON COLUMN geohub.dataset_permission.permission - IS '1: read, 2: read/write, 3: owner'; - --- style_permission table -CREATE TABLE IF NOT EXISTS geohub.style_permission -( - style_id integer NOT NULL, - user_email character varying(100) NOT NULL, - permission smallint NOT NULL DEFAULT 1, - createdat timestamp with time zone NOT NULL DEFAULT now(), - updatedat timestamp with time zone, - CONSTRAINT style_permission_pkey PRIMARY KEY (style_id, user_email), - CONSTRAINT "FK_style_TO_style_permission" FOREIGN KEY (style_id) - REFERENCES geohub.style (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE CASCADE - NOT VALID -); - -COMMENT ON TABLE geohub.style_permission - IS 'this table manages users'' permission for operating each saved style'; - -COMMENT ON COLUMN geohub.style_permission.permission - IS '1: read, 2: read/write, 3: owner'; - -CREATE TABLE IF NOT EXISTS geohub.user_settings -( - user_email character varying(100) NOT NULL, - settings json NOT NULL, - CONSTRAINT user_settings_pkey PRIMARY KEY (user_email) -); - --- ALTER TABLE IF EXISTS geohub.user_settings --- OWNER to undpgeohub; - -COMMENT ON TABLE geohub.user_settings - IS 'This table stores user settings'; - -COMMENT ON COLUMN geohub.user_settings.user_email - IS 'user email address'; - -COMMENT ON COLUMN geohub.user_settings.settings - IS 'This column stores user settings in json format'; - -CREATE TABLE IF NOT EXISTS geohub.users -( - id character varying NOT NULL, - user_email character varying(100) NOT NULL, - signupat timestamp with time zone NOT NULL DEFAULT now(), - lastaccessedat timestamp with time zone NOT NULL DEFAULT now(), - CONSTRAINT users_pkey PRIMARY KEY (id) -); - --- ALTER TABLE IF EXISTS geohub.users --- OWNER to undpgeohub; - -COMMENT ON TABLE geohub.users - IS 'This table manages the login users information for analysis'; - -COMMENT ON COLUMN geohub.users.id - IS 'MD5 hash key from user email address'; - -COMMENT ON COLUMN geohub.users.user_email - IS 'Login user email address'; - -COMMENT ON COLUMN geohub.users.signupat - IS 'date time when user first time accessed'; - -COMMENT ON COLUMN geohub.users.lastaccessedat - IS 'date time when user accessed last time'; - -CREATE TABLE IF NOT EXISTS geohub.stac -( - id character varying NOT NULL, - name character varying NOT NULL, - url character varying NOT NULL, - type character varying NOT NULL, - providers json, - createdat timestamp with time zone NOT NULL DEFAULT now(), - created_user character varying(100) NOT NULL, - updatedat timestamp with time zone, - updated_user character varying(100) , - CONSTRAINT stac_pkey PRIMARY KEY (id) -); - -COMMENT ON TABLE geohub.stac - IS 'This table is to manage STAC APIs and Catalogs managed in GeoHub'; - -COMMENT ON COLUMN geohub.stac.id - IS 'STAC ID'; - -COMMENT ON COLUMN geohub.stac.name - IS 'STAC name'; - -COMMENT ON COLUMN geohub.stac.url - IS 'STAC API or Catalog.json URL'; - -COMMENT ON COLUMN geohub.stac.type - IS 'either api or catalog'; - -COMMENT ON COLUMN geohub.stac.providers - IS 'json of array of provider name'; - -CREATE TABLE IF NOT EXISTS geohub.product -( - id character varying NOT NULL, - label character varying NOT NULL, - expression character varying NOT NULL, - description character varying NOT NULL, - CONSTRAINT product_pkey PRIMARY KEY (id) - INCLUDE(id) -); - -COMMENT ON TABLE geohub.product - IS 'This is the table that manages the products'; - -COMMENT ON COLUMN geohub.product.id - IS 'Id column of the product. Must be unique'; - -CREATE TABLE IF NOT EXISTS geohub.stac_collection_product -( - stac_id character varying NOT NULL, - collection_id character varying NOT NULL, - product_id character varying NOT NULL, - assets character varying[] NOT NULL, - description character varying[] NOT NULL, - CONSTRAINT stac_collection_product_pkey PRIMARY KEY (stac_id, collection_id, product_id) - INCLUDE(stac_id, collection_id, product_id), - CONSTRAINT stac_collection_product_product_id_fkey FOREIGN KEY (product_id) - REFERENCES geohub.product (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE NO ACTION - NOT VALID -); - -COMMENT ON TABLE geohub.stac_collection_product - IS 'This is the table to manage linking a product to stac collection.'; - --------------------------- --- Storymap tables --------------------------- - -CREATE TABLE IF NOT EXISTS geohub.storymap -( - id uuid NOT NULL, - title character varying NOT NULL, - logo character varying, - subtitle character varying, - byline character varying, - footer character varying, - template_id character varying NOT NULL, - style_id integer, - base_style_id character varying, - access_level integer NOT NULL DEFAULT 1, - showProgress boolean NOT NULL DEFAULT true, - createdat timestamp with time zone NOT NULL, - created_user character varying NOT NULL, - updatedat timestamp with time zone, - updated_user character varying, - CONSTRAINT storymap_pkey PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS geohub.storymap_chapter -( - id uuid NOT NULL, - title character varying NOT NULL, - description character varying NOT NULL, - image character varying, - card_hidden boolean NOT NULL DEFAULT false, - alignment character varying NOT NULL, - map_interactive boolean NOT NULL DEFAULT false, - map_navigation_position character varying NOT NULL, - map_animation character varying NOT NULL, - rotate_animation boolean NOT NULL DEFAULT false, - spinglobe boolean NOT NULL DEFAULT false, - hidden boolean NOT NULL DEFAULT false, - center geometry(Point,4326) NOT NULL, - zoom double precision NOT NULL, - bearing double precision NOT NULL DEFAULT 0, - pitch double precision NOT NULL DEFAULT 0, - style_id integer, - base_style_id character varying, - on_chapter_enter jsonb, - on_chapter_exit jsonb, - legend_position character varying NOT NULL DEFAULT 'bottom-left', - show_legend boolean NOT NULL DEFAULT true, - createdat timestamp with time zone NOT NULL, - created_user character varying NOT NULL, - updatedat timestamp with time zone, - updated_user character varying, - CONSTRAINT storymap_chapter_pkey PRIMARY KEY (id) -); - -CREATE TABLE IF NOT EXISTS geohub.storymap_chapters -( - storymap_id uuid NOT NULL, - chapter_id uuid NOT NULL, - sequence integer NOT NULL, - CONSTRAINT storymap_chapters_pkey PRIMARY KEY (storymap_id, chapter_id), - CONSTRAINT fk_storymap_to_storymap_chapters FOREIGN KEY (storymap_id) - REFERENCES geohub.storymap (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE CASCADE, - CONSTRAINT fk_storymap_to_storymap_chapter FOREIGN KEY (chapter_id) - REFERENCES geohub.storymap_chapter (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE CASCADE -); - -CREATE TABLE IF NOT EXISTS geohub.storymap_favourite -( - storymap_id uuid NOT NULL, - user_email character varying(100) NOT NULL, - savedat timestamp with time zone NOT NULL, - CONSTRAINT storymap_favourite_pkey PRIMARY KEY (storymap_id, user_email), - CONSTRAINT "FK_storymap_TO_storymap_favourite" FOREIGN KEY (storymap_id) - REFERENCES geohub.storymap (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE CASCADE - NOT VALID -); - -CREATE TABLE IF NOT EXISTS geohub.storymap_permission -( - storymap_id uuid NOT NULL, - user_email character varying(100) NOT NULL, - permission smallint NOT NULL DEFAULT 1, - createdat timestamp with time zone NOT NULL DEFAULT now(), - updatedat timestamp with time zone, - CONSTRAINT storymap_permission_pkey PRIMARY KEY (storymap_id, user_email), - CONSTRAINT "FK_storymap_TO_storymap_permission" FOREIGN KEY (storymap_id) - REFERENCES geohub.storymap (id) MATCH SIMPLE - ON UPDATE NO ACTION - ON DELETE CASCADE - NOT VALID -); - -CREATE TABLE IF NOT EXISTS geohub.license -( - id serial NOT NULL, - name character varying NOT NULL, - PRIMARY KEY (id) -); \ No newline at end of file diff --git a/backends/database/init.sh b/backends/database/init.sh index 446460884..28ee717bb 100755 --- a/backends/database/init.sh +++ b/backends/database/init.sh @@ -4,12 +4,6 @@ DATABASE=geodata HOST=localhost POST=25432 -echo -echo "------------------------------------------------------------------" -echo "Initialising geodata database" -echo "------------------------------------------------------------------" -psql -f ./backends/database/geohub-database.sql -U $USER -d $DATABASE -h $HOST -p $POST - echo echo "------------------------------------------------------------------" echo "Inserting datasets for Electricity Dashboard" diff --git a/docker/docker-run-prod.sh b/docker/docker-run-prod.sh index e5b7afa78..5a789166b 100755 --- a/docker/docker-run-prod.sh +++ b/docker/docker-run-prod.sh @@ -5,7 +5,7 @@ if [ -z $IMAGE_NAME ]; then fi if [ -z $PORT ]; then - PORT=3000 + PORT=5173 fi # load environmental variables diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 867f896b7..51876969f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -863,18 +863,21 @@ importers: crypto-js: specifier: ^4.2.0 version: 4.2.0 + drizzle-orm: + specifier: ^0.33.0 + version: 0.33.0(@types/pg@8.11.10)(@types/react@18.3.10)(pg@8.13.0)(postgres@3.4.4)(react@18.3.1) jwt-decode: specifier: ^4.0.0 version: 4.0.0 pbf: specifier: ^4.0.1 version: 4.0.1 - pg: - specifier: ^8.13.0 - version: 8.13.0 pngjs: specifier: ^7.0.0 version: 7.0.0 + postgres: + specifier: ^3.4.4 + version: 3.4.4 devDependencies: '@auth/core': specifier: ^0.35.3 @@ -1032,6 +1035,9 @@ importers: dayjs: specifier: ^1.11.13 version: 1.11.13 + drizzle-kit: + specifier: ^0.24.2 + version: 0.24.2 easymde: specifier: ^2.18.0 version: 2.18.0 @@ -1433,6 +1439,21 @@ packages: '@changesets/write@0.3.2': resolution: {integrity: sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw==} + '@drizzle-team/brocli@0.10.1': + resolution: {integrity: sha512-AHy0vjc+n/4w/8Mif+w86qpppHuF3AyXbcWW+R/W7GNA3F5/p2nuhlkCJaTXSLZheB4l1rtHzOfr9A7NwoR/Zg==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + + '@esbuild/aix-ppc64@0.19.12': + resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -1445,6 +1466,18 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.19.12': + resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -1457,6 +1490,18 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.19.12': + resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -1469,6 +1514,18 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.19.12': + resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -1481,6 +1538,18 @@ packages: cpu: [x64] os: [android] + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.19.12': + resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -1493,6 +1562,18 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.19.12': + resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -1505,6 +1586,18 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.19.12': + resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -1517,6 +1610,18 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.19.12': + resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -1529,6 +1634,18 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.19.12': + resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -1541,6 +1658,18 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.19.12': + resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -1553,6 +1682,18 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.19.12': + resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -1565,6 +1706,18 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.19.12': + resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -1577,6 +1730,18 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.19.12': + resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -1589,6 +1754,18 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.19.12': + resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -1601,6 +1778,18 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.19.12': + resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -1613,6 +1802,18 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.19.12': + resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -1625,6 +1826,18 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.19.12': + resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -1637,6 +1850,18 @@ packages: cpu: [x64] os: [linux] + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.19.12': + resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -1655,6 +1880,18 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.19.12': + resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -1667,6 +1904,18 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.19.12': + resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -1679,6 +1928,18 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.19.12': + resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -1691,6 +1952,18 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.19.12': + resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -1703,6 +1976,18 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.19.12': + resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -2950,6 +3235,9 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -3436,6 +3724,99 @@ packages: domutils@2.8.0: resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + drizzle-kit@0.24.2: + resolution: {integrity: sha512-nXOaTSFiuIaTMhS8WJC2d4EBeIcN9OSt2A2cyFbQYBAZbi7lRsVGJNqDpEwPqYfJz38yxbY/UtbvBBahBfnExQ==} + hasBin: true + + drizzle-orm@0.33.0: + resolution: {integrity: sha512-SHy72R2Rdkz0LEq0PSG/IdvnT3nGiWuRk+2tXZQ90GVq/XQhpCzu/EFT3V2rox+w8MlkBQxifF8pCStNYnERfA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=3' + '@electric-sql/pglite': '>=0.1.1' + '@libsql/client': '*' + '@neondatabase/serverless': '>=0.1' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/react': '>=18' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=13.2.0' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + react: '>=18' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/react': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + react: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + earcut@3.0.0: resolution: {integrity: sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==} @@ -3501,6 +3882,16 @@ packages: peerDependencies: esbuild: '>=0.12 <1' + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.19.12: + resolution: {integrity: sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==} + engines: {node: '>=12'} + hasBin: true + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -3847,6 +4238,9 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + get-tsconfig@4.8.1: + resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} + get-value@2.0.6: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} engines: {node: '>=0.10.0'} @@ -5019,6 +5413,10 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + postgres@3.4.4: + resolution: {integrity: sha512-IbyN+9KslkqcXa8AO9fxpk97PA4pzewvpi2B3Dwy9u4zpV32QicaEdgmF3eSQUzdRk7ttDHQejNgAEr4XoeH4A==} + engines: {node: '>=12'} + potpack@2.0.0: resolution: {integrity: sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==} @@ -5186,6 +5584,9 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-protobuf-schema@2.1.0: resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} @@ -5356,6 +5757,9 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -6584,108 +6988,225 @@ snapshots: human-id: 1.0.2 prettier: 2.8.8 + '@drizzle-team/brocli@0.10.1': {} + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.8.1 + + '@esbuild/aix-ppc64@0.19.12': + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true '@esbuild/aix-ppc64@0.23.1': optional: true + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.19.12': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true '@esbuild/android-arm64@0.23.1': optional: true + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.19.12': + optional: true + '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-arm@0.23.1': optional: true + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.19.12': + optional: true + '@esbuild/android-x64@0.21.5': optional: true '@esbuild/android-x64@0.23.1': optional: true + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.19.12': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true '@esbuild/darwin-arm64@0.23.1': optional: true + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.19.12': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true '@esbuild/darwin-x64@0.23.1': optional: true + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.19.12': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true '@esbuild/freebsd-arm64@0.23.1': optional: true + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.19.12': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true '@esbuild/freebsd-x64@0.23.1': optional: true + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.19.12': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true '@esbuild/linux-arm64@0.23.1': optional: true + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.19.12': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true '@esbuild/linux-arm@0.23.1': optional: true + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.19.12': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true '@esbuild/linux-ia32@0.23.1': optional: true + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.19.12': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true '@esbuild/linux-loong64@0.23.1': optional: true + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.19.12': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true '@esbuild/linux-mips64el@0.23.1': optional: true + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.19.12': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true '@esbuild/linux-ppc64@0.23.1': optional: true + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.19.12': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true '@esbuild/linux-riscv64@0.23.1': optional: true + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.19.12': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true '@esbuild/linux-s390x@0.23.1': optional: true + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.19.12': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true '@esbuild/linux-x64@0.23.1': optional: true + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.19.12': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true @@ -6695,30 +7216,60 @@ snapshots: '@esbuild/openbsd-arm64@0.23.1': optional: true + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.19.12': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true '@esbuild/openbsd-x64@0.23.1': optional: true + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.19.12': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true '@esbuild/sunos-x64@0.23.1': optional: true + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.19.12': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true '@esbuild/win32-arm64@0.23.1': optional: true + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.19.12': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true '@esbuild/win32-ia32@0.23.1': optional: true + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.19.12': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true @@ -8243,6 +8794,8 @@ snapshots: buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -8728,6 +9281,23 @@ snapshots: domelementtype: 2.3.0 domhandler: 4.3.1 + drizzle-kit@0.24.2: + dependencies: + '@drizzle-team/brocli': 0.10.1 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.19.12 + esbuild-register: 3.6.0(esbuild@0.19.12) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.33.0(@types/pg@8.11.10)(@types/react@18.3.10)(pg@8.13.0)(postgres@3.4.4)(react@18.3.1): + optionalDependencies: + '@types/pg': 8.11.10 + '@types/react': 18.3.10 + pg: 8.13.0 + postgres: 3.4.4 + react: 18.3.1 + earcut@3.0.0: {} eastasianwidth@0.2.0: {} @@ -8781,6 +9351,13 @@ snapshots: es6-promise@3.3.1: {} + esbuild-register@3.6.0(esbuild@0.19.12): + dependencies: + debug: 4.3.7 + esbuild: 0.19.12 + transitivePeerDependencies: + - supports-color + esbuild-register@3.6.0(esbuild@0.23.1): dependencies: debug: 4.3.7 @@ -8788,6 +9365,57 @@ snapshots: transitivePeerDependencies: - supports-color + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.19.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.19.12 + '@esbuild/android-arm': 0.19.12 + '@esbuild/android-arm64': 0.19.12 + '@esbuild/android-x64': 0.19.12 + '@esbuild/darwin-arm64': 0.19.12 + '@esbuild/darwin-x64': 0.19.12 + '@esbuild/freebsd-arm64': 0.19.12 + '@esbuild/freebsd-x64': 0.19.12 + '@esbuild/linux-arm': 0.19.12 + '@esbuild/linux-arm64': 0.19.12 + '@esbuild/linux-ia32': 0.19.12 + '@esbuild/linux-loong64': 0.19.12 + '@esbuild/linux-mips64el': 0.19.12 + '@esbuild/linux-ppc64': 0.19.12 + '@esbuild/linux-riscv64': 0.19.12 + '@esbuild/linux-s390x': 0.19.12 + '@esbuild/linux-x64': 0.19.12 + '@esbuild/netbsd-x64': 0.19.12 + '@esbuild/openbsd-x64': 0.19.12 + '@esbuild/sunos-x64': 0.19.12 + '@esbuild/win32-arm64': 0.19.12 + '@esbuild/win32-ia32': 0.19.12 + '@esbuild/win32-x64': 0.19.12 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -9267,6 +9895,10 @@ snapshots: get-stream@6.0.1: {} + get-tsconfig@4.8.1: + dependencies: + resolve-pkg-maps: 1.0.0 + get-value@2.0.6: {} github-slugger@2.0.0: {} @@ -10431,7 +11063,8 @@ snapshots: pg-cloudflare@1.1.1: optional: true - pg-connection-string@2.7.0: {} + pg-connection-string@2.7.0: + optional: true pg-int8@1.0.1: {} @@ -10440,6 +11073,7 @@ snapshots: pg-pool@3.7.0(pg@8.13.0): dependencies: pg: 8.13.0 + optional: true pg-protocol@1.7.0: {} @@ -10450,6 +11084,7 @@ snapshots: postgres-bytea: 1.0.0 postgres-date: 1.0.7 postgres-interval: 1.2.0 + optional: true pg-types@4.0.2: dependencies: @@ -10470,10 +11105,12 @@ snapshots: pgpass: 1.0.5 optionalDependencies: pg-cloudflare: 1.1.1 + optional: true pgpass@1.0.5: dependencies: split2: 4.2.0 + optional: true picocolors@1.1.0: {} @@ -10529,28 +11166,34 @@ snapshots: picocolors: 1.1.0 source-map-js: 1.2.1 - postgres-array@2.0.0: {} + postgres-array@2.0.0: + optional: true postgres-array@3.0.2: {} - postgres-bytea@1.0.0: {} + postgres-bytea@1.0.0: + optional: true postgres-bytea@3.0.0: dependencies: obuf: 1.1.2 - postgres-date@1.0.7: {} + postgres-date@1.0.7: + optional: true postgres-date@2.1.0: {} postgres-interval@1.2.0: dependencies: xtend: 4.0.2 + optional: true postgres-interval@3.0.0: {} postgres-range@1.1.4: {} + postgres@3.4.4: {} + potpack@2.0.0: {} preact-render-to-string@5.2.3(preact@10.11.3): @@ -10732,6 +11375,8 @@ snapshots: resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve-protobuf-schema@2.1.0: dependencies: protocol-buffers-schema: 3.6.0 @@ -10941,6 +11586,11 @@ snapshots: source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map@0.6.1: {} space-separated-tokens@2.0.2: {} @@ -10956,7 +11606,8 @@ snapshots: dependencies: extend-shallow: 3.0.2 - split2@4.2.0: {} + split2@4.2.0: + optional: true sprintf-js@1.0.3: {} diff --git a/sites/geohub/drizzle.config.ts b/sites/geohub/drizzle.config.ts new file mode 100644 index 000000000..a8ab66f93 --- /dev/null +++ b/sites/geohub/drizzle.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/lib/server/schema.ts', + out: './drizzle', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DATABASE_CONNECTION! + }, + schemaFilter: ['geohub'], + extensionsFilters: ['postgis'] +}); diff --git a/sites/geohub/drizzle/0000_conscious_madelyne_pryor.sql b/sites/geohub/drizzle/0000_conscious_madelyne_pryor.sql new file mode 100644 index 000000000..612e3febb --- /dev/null +++ b/sites/geohub/drizzle/0000_conscious_madelyne_pryor.sql @@ -0,0 +1,301 @@ +-- Current sql file was generated after introspecting the database +-- If you want to run this migration please uncomment this code before executing migrations + +CREATE SCHEMA "geohub"; +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."country" ( + "iso_3" varchar PRIMARY KEY NOT NULL, + "iso_code" integer NOT NULL, + "iso_2" varchar, + "name" varchar NOT NULL, + "region1_code" integer NOT NULL, + "region1_name" varchar NOT NULL, + "region2_code" integer NOT NULL, + "region2_name" varchar NOT NULL, + "region3_code" integer NOT NULL, + "region3_name" varchar NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."user_settings" ( + "user_email" varchar(100) NOT NULL, + "settings" json, + CONSTRAINT "constraint_name" UNIQUE("user_email") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."tag" ( + "id" serial PRIMARY KEY NOT NULL, + "value" varchar NOT NULL, + "key" varchar NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."style" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar NOT NULL, + "style" json NOT NULL, + "createdat" timestamp with time zone DEFAULT now() NOT NULL, + "updatedat" timestamp with time zone DEFAULT now() NOT NULL, + "layers" json, + "access_level" integer DEFAULT 1 NOT NULL, + "created_user" varchar(100), + "updated_user" varchar(100) +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."users" ( + "id" varchar PRIMARY KEY NOT NULL, + "user_email" varchar(100) NOT NULL, + "signupat" timestamp with time zone DEFAULT now() NOT NULL, + "lastaccessedat" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."dataset" ( + "id" varchar PRIMARY KEY NOT NULL, + "url" varchar NOT NULL, + "is_raster" boolean NOT NULL, + "license" varchar, + "bounds" geometry(Polygon,4326) NOT NULL, + "createdat" timestamp with time zone NOT NULL, + "updatedat" timestamp with time zone, + "name" varchar, + "description" varchar, + "created_user" varchar(100) NOT NULL, + "updated_user" varchar(100), + "access_level" integer DEFAULT 3 NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."superuser" ( + "user_email" varchar(100) PRIMARY KEY NOT NULL, + "createdat" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."stac" ( + "id" varchar PRIMARY KEY NOT NULL, + "name" varchar NOT NULL, + "url" varchar NOT NULL, + "type" varchar NOT NULL, + "providers" json, + "createdat" timestamp with time zone DEFAULT now() NOT NULL, + "created_user" varchar(100) NOT NULL, + "updatedat" timestamp with time zone, + "updated_user" varchar(100) +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."product" ( + "id" varchar PRIMARY KEY NOT NULL, + "label" varchar NOT NULL, + "expression" varchar NOT NULL, + "description" varchar NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."storymap" ( + "id" uuid PRIMARY KEY NOT NULL, + "title" varchar NOT NULL, + "logo" varchar, + "subtitle" varchar, + "byline" varchar, + "footer" varchar, + "template_id" varchar NOT NULL, + "style_id" integer, + "base_style_id" varchar, + "access_level" integer DEFAULT 1 NOT NULL, + "createdat" timestamp with time zone NOT NULL, + "created_user" varchar NOT NULL, + "updatedat" timestamp with time zone, + "updated_user" varchar, + "show_progress" boolean DEFAULT true NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."storymap_chapter" ( + "id" uuid PRIMARY KEY NOT NULL, + "title" varchar NOT NULL, + "description" varchar NOT NULL, + "image" varchar, + "alignment" varchar NOT NULL, + "map_interactive" boolean DEFAULT false NOT NULL, + "map_navigation_position" varchar NOT NULL, + "map_animation" varchar NOT NULL, + "rotate_animation" boolean DEFAULT false NOT NULL, + "spinglobe" boolean DEFAULT false NOT NULL, + "hidden" boolean DEFAULT false NOT NULL, + "center" geometry(Point,4326) NOT NULL, + "zoom" double precision NOT NULL, + "bearing" double precision DEFAULT 0 NOT NULL, + "pitch" double precision DEFAULT 0 NOT NULL, + "style_id" integer, + "base_style_id" varchar, + "on_chapter_enter" jsonb, + "on_chapter_exit" jsonb, + "createdat" timestamp with time zone NOT NULL, + "created_user" varchar NOT NULL, + "updatedat" timestamp with time zone, + "updated_user" varchar, + "card_hidden" boolean DEFAULT false NOT NULL, + "legend_position" varchar DEFAULT 'bottom-left', + "show_legend" boolean DEFAULT true NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."license" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."dataset_tag" ( + "dataset_id" varchar NOT NULL, + "tag_id" serial NOT NULL, + CONSTRAINT "dataset_tag_pkey" PRIMARY KEY("dataset_id","tag_id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."style_favourite" ( + "style_id" integer NOT NULL, + "user_email" varchar(100) NOT NULL, + "savedat" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "style_favourite_pkey" PRIMARY KEY("style_id","user_email") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."dataset_favourite" ( + "dataset_id" varchar NOT NULL, + "user_email" varchar(100) NOT NULL, + "savedat" timestamp with time zone NOT NULL, + CONSTRAINT "dataset_favourite_pkey" PRIMARY KEY("dataset_id","user_email") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."storymap_chapters" ( + "storymap_id" uuid NOT NULL, + "chapter_id" uuid NOT NULL, + "sequence" integer NOT NULL, + CONSTRAINT "storymap_chapters_pkey" PRIMARY KEY("storymap_id","chapter_id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."storymap_favourite" ( + "storymap_id" uuid NOT NULL, + "user_email" varchar(100) NOT NULL, + "savedat" timestamp with time zone NOT NULL, + CONSTRAINT "storymap_favourite_pkey" PRIMARY KEY("storymap_id","user_email") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."dataset_permission" ( + "dataset_id" varchar NOT NULL, + "user_email" varchar(100) NOT NULL, + "permission" smallint DEFAULT 1 NOT NULL, + "createdat" timestamp with time zone DEFAULT now() NOT NULL, + "updatedat" timestamp with time zone, + CONSTRAINT "dataset_permission_pkey" PRIMARY KEY("dataset_id","user_email") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."stac_collection_product" ( + "stac_id" varchar NOT NULL, + "collection_id" varchar NOT NULL, + "product_id" varchar NOT NULL, + "assets" varchar[] NOT NULL, + "description" varchar, + CONSTRAINT "stac_collection_product_pkey" PRIMARY KEY("stac_id","collection_id","product_id") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."style_permission" ( + "style_id" integer NOT NULL, + "user_email" varchar(100) NOT NULL, + "permission" smallint DEFAULT 1 NOT NULL, + "createdat" timestamp with time zone DEFAULT now() NOT NULL, + "updatedat" timestamp with time zone, + CONSTRAINT "style_permission_pkey" PRIMARY KEY("style_id","user_email") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."storymap_permission" ( + "storymap_id" uuid NOT NULL, + "user_email" varchar(100) NOT NULL, + "permission" smallint DEFAULT 1 NOT NULL, + "createdat" timestamp with time zone DEFAULT now() NOT NULL, + "updatedat" timestamp with time zone, + CONSTRAINT "storymap_permission_pkey" PRIMARY KEY("storymap_id","user_email") +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "geohub"."dataset_defaultstyle" ( + "dataset_id" varchar NOT NULL, + "layer_id" varchar NOT NULL, + "layer_type" varchar NOT NULL, + "source" json NOT NULL, + "style" json NOT NULL, + "colormap_name" varchar, + "classification_method" varchar, + "created_user" varchar(100) NOT NULL, + "createdat" timestamp with time zone DEFAULT now() NOT NULL, + "updatedat" timestamp with time zone, + "updated_user" varchar(100), + "classification_method_2" varchar, + CONSTRAINT "dataset_defaultstyle_pkey" PRIMARY KEY("dataset_id","layer_id","layer_type") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "geohub"."dataset_tag" ADD CONSTRAINT "fk_dataset_to_dataset_tag" FOREIGN KEY ("dataset_id") REFERENCES "geohub"."dataset"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "geohub"."dataset_tag" ADD CONSTRAINT "fk_tag_to_dataset_tag" FOREIGN KEY ("tag_id") REFERENCES "geohub"."tag"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "geohub"."style_favourite" ADD CONSTRAINT "FK_style_TO_style_favourite" FOREIGN KEY ("style_id") REFERENCES "geohub"."style"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "geohub"."dataset_favourite" ADD CONSTRAINT "FK_dataset_TO_dataset_favourite" FOREIGN KEY ("dataset_id") REFERENCES "geohub"."dataset"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "geohub"."storymap_chapters" ADD CONSTRAINT "fk_storymap_to_storymap_chapters" FOREIGN KEY ("storymap_id") REFERENCES "geohub"."storymap"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "geohub"."storymap_chapters" ADD CONSTRAINT "fk_storymap_to_storymap_chapter" FOREIGN KEY ("chapter_id") REFERENCES "geohub"."storymap_chapter"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "geohub"."storymap_favourite" ADD CONSTRAINT "FK_storymap_TO_storymap_favourite" FOREIGN KEY ("storymap_id") REFERENCES "geohub"."storymap"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "geohub"."dataset_permission" ADD CONSTRAINT "FK_dataset_TO_dataset_permission" FOREIGN KEY ("dataset_id") REFERENCES "geohub"."dataset"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "geohub"."stac_collection_product" ADD CONSTRAINT "stac_collection_product_product_id_fkey" FOREIGN KEY ("product_id") REFERENCES "geohub"."product"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "geohub"."style_permission" ADD CONSTRAINT "FK_style_TO_style_permission" FOREIGN KEY ("style_id") REFERENCES "geohub"."style"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "geohub"."storymap_permission" ADD CONSTRAINT "FK_storymap_TO_storymap_permission" FOREIGN KEY ("storymap_id") REFERENCES "geohub"."storymap"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "geohub"."dataset_defaultstyle" ADD CONSTRAINT "FK_dataset_TO_dataset_id, layer_type" FOREIGN KEY ("dataset_id") REFERENCES "geohub"."dataset"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "tag_idx_key_value" ON "geohub"."tag" USING btree ("key","value");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "tag_idx_value" ON "geohub"."tag" USING btree ("value");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "dataset_bounds_geom_idx" ON "geohub"."dataset" USING gist ("bounds"); diff --git a/sites/geohub/drizzle/meta/0000_snapshot.json b/sites/geohub/drizzle/meta/0000_snapshot.json new file mode 100644 index 000000000..dc08c772b --- /dev/null +++ b/sites/geohub/drizzle/meta/0000_snapshot.json @@ -0,0 +1,1467 @@ +{ + "id": "00000000-0000-0000-0000-000000000000", + "prevId": "", + "version": "7", + "dialect": "postgresql", + "tables": { + "geohub.country": { + "name": "country", + "schema": "geohub", + "columns": { + "iso_3": { + "name": "iso_3", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "iso_code": { + "name": "iso_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "iso_2": { + "name": "iso_2", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "region1_code": { + "name": "region1_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "region1_name": { + "name": "region1_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "region2_code": { + "name": "region2_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "region2_name": { + "name": "region2_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "region3_code": { + "name": "region3_code", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "region3_name": { + "name": "region3_name", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "geohub.user_settings": { + "name": "user_settings", + "schema": "geohub", + "columns": { + "user_email": { + "name": "user_email", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "settings": { + "name": "settings", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "constraint_name": { + "columns": [ + "user_email" + ], + "nullsNotDistinct": false, + "name": "constraint_name" + } + } + }, + "geohub.tag": { + "name": "tag", + "schema": "geohub", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "value": { + "name": "value", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "tag_idx_key_value": { + "name": "tag_idx_key_value", + "columns": [ + { + "expression": "key", + "asc": true, + "nulls": "last", + "opclass": "text_ops", + "isExpression": false + }, + { + "expression": "value", + "asc": true, + "nulls": "last", + "opclass": "text_ops", + "isExpression": false + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tag_idx_value": { + "name": "tag_idx_value", + "columns": [ + { + "expression": "value", + "asc": true, + "nulls": "last", + "opclass": "text_ops", + "isExpression": false + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "geohub.style": { + "name": "style", + "schema": "geohub", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "style": { + "name": "style", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "createdat": { + "name": "createdat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedat": { + "name": "updatedat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "layers": { + "name": "layers", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "access_level": { + "name": "access_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_user": { + "name": "created_user", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "updated_user": { + "name": "updated_user", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "geohub.users": { + "name": "users", + "schema": "geohub", + "columns": { + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "signupat": { + "name": "signupat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "lastaccessedat": { + "name": "lastaccessedat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "geohub.dataset": { + "name": "dataset", + "schema": "geohub", + "columns": { + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "is_raster": { + "name": "is_raster", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "license": { + "name": "license", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "bounds": { + "name": "bounds", + "type": "geometry(Polygon,4326)", + "primaryKey": false, + "notNull": true + }, + "createdat": { + "name": "createdat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updatedat": { + "name": "updatedat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_user": { + "name": "created_user", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "updated_user": { + "name": "updated_user", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "access_level": { + "name": "access_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + } + }, + "indexes": { + "dataset_bounds_geom_idx": { + "name": "dataset_bounds_geom_idx", + "columns": [ + { + "expression": "bounds", + "asc": true, + "nulls": "last", + "opclass": "gist_geometry_ops_2d", + "isExpression": false + } + ], + "isUnique": false, + "concurrently": false, + "method": "gist", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "geohub.superuser": { + "name": "superuser", + "schema": "geohub", + "columns": { + "user_email": { + "name": "user_email", + "type": "varchar(100)", + "primaryKey": true, + "notNull": true + }, + "createdat": { + "name": "createdat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "geohub.stac": { + "name": "stac", + "schema": "geohub", + "columns": { + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "providers": { + "name": "providers", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "createdat": { + "name": "createdat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_user": { + "name": "created_user", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "updatedat": { + "name": "updatedat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_user": { + "name": "updated_user", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "geohub.product": { + "name": "product", + "schema": "geohub", + "columns": { + "id": { + "name": "id", + "type": "varchar", + "primaryKey": true, + "notNull": true + }, + "label": { + "name": "label", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "expression": { + "name": "expression", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "geohub.storymap": { + "name": "storymap", + "schema": "geohub", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "subtitle": { + "name": "subtitle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "byline": { + "name": "byline", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "footer": { + "name": "footer", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "template_id": { + "name": "template_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "style_id": { + "name": "style_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "base_style_id": { + "name": "base_style_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "access_level": { + "name": "access_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdat": { + "name": "createdat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_user": { + "name": "created_user", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "updatedat": { + "name": "updatedat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_user": { + "name": "updated_user", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "show_progress": { + "name": "show_progress", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "geohub.storymap_chapter": { + "name": "storymap_chapter", + "schema": "geohub", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "alignment": { + "name": "alignment", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "map_interactive": { + "name": "map_interactive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "map_navigation_position": { + "name": "map_navigation_position", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "map_animation": { + "name": "map_animation", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "rotate_animation": { + "name": "rotate_animation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "spinglobe": { + "name": "spinglobe", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "hidden": { + "name": "hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "center": { + "name": "center", + "type": "geometry(Point,4326)", + "primaryKey": false, + "notNull": true + }, + "zoom": { + "name": "zoom", + "type": "double precision", + "primaryKey": false, + "notNull": true + }, + "bearing": { + "name": "bearing", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pitch": { + "name": "pitch", + "type": "double precision", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "style_id": { + "name": "style_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "base_style_id": { + "name": "base_style_id", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "on_chapter_enter": { + "name": "on_chapter_enter", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "on_chapter_exit": { + "name": "on_chapter_exit", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "createdat": { + "name": "createdat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_user": { + "name": "created_user", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "updatedat": { + "name": "updatedat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_user": { + "name": "updated_user", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "card_hidden": { + "name": "card_hidden", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "legend_position": { + "name": "legend_position", + "type": "varchar", + "primaryKey": false, + "notNull": false, + "default": "'bottom-left'" + }, + "show_legend": { + "name": "show_legend", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "geohub.license": { + "name": "license", + "schema": "geohub", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "geohub.dataset_tag": { + "name": "dataset_tag", + "schema": "geohub", + "columns": { + "dataset_id": { + "name": "dataset_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "serial", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "fk_dataset_to_dataset_tag": { + "name": "fk_dataset_to_dataset_tag", + "tableFrom": "dataset_tag", + "tableTo": "dataset", + "schemaTo": "geohub", + "columnsFrom": [ + "dataset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fk_tag_to_dataset_tag": { + "name": "fk_tag_to_dataset_tag", + "tableFrom": "dataset_tag", + "tableTo": "tag", + "schemaTo": "geohub", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dataset_tag_pkey": { + "name": "dataset_tag_pkey", + "columns": [ + "dataset_id", + "tag_id" + ] + } + }, + "uniqueConstraints": {} + }, + "geohub.style_favourite": { + "name": "style_favourite", + "schema": "geohub", + "columns": { + "style_id": { + "name": "style_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "savedat": { + "name": "savedat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "FK_style_TO_style_favourite": { + "name": "FK_style_TO_style_favourite", + "tableFrom": "style_favourite", + "tableTo": "style", + "schemaTo": "geohub", + "columnsFrom": [ + "style_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "style_favourite_pkey": { + "name": "style_favourite_pkey", + "columns": [ + "style_id", + "user_email" + ] + } + }, + "uniqueConstraints": {} + }, + "geohub.dataset_favourite": { + "name": "dataset_favourite", + "schema": "geohub", + "columns": { + "dataset_id": { + "name": "dataset_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "savedat": { + "name": "savedat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "FK_dataset_TO_dataset_favourite": { + "name": "FK_dataset_TO_dataset_favourite", + "tableFrom": "dataset_favourite", + "tableTo": "dataset", + "schemaTo": "geohub", + "columnsFrom": [ + "dataset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dataset_favourite_pkey": { + "name": "dataset_favourite_pkey", + "columns": [ + "dataset_id", + "user_email" + ] + } + }, + "uniqueConstraints": {} + }, + "geohub.storymap_chapters": { + "name": "storymap_chapters", + "schema": "geohub", + "columns": { + "storymap_id": { + "name": "storymap_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "chapter_id": { + "name": "chapter_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sequence": { + "name": "sequence", + "type": "integer", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "fk_storymap_to_storymap_chapters": { + "name": "fk_storymap_to_storymap_chapters", + "tableFrom": "storymap_chapters", + "tableTo": "storymap", + "schemaTo": "geohub", + "columnsFrom": [ + "storymap_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "fk_storymap_to_storymap_chapter": { + "name": "fk_storymap_to_storymap_chapter", + "tableFrom": "storymap_chapters", + "tableTo": "storymap_chapter", + "schemaTo": "geohub", + "columnsFrom": [ + "chapter_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "storymap_chapters_pkey": { + "name": "storymap_chapters_pkey", + "columns": [ + "storymap_id", + "chapter_id" + ] + } + }, + "uniqueConstraints": {} + }, + "geohub.storymap_favourite": { + "name": "storymap_favourite", + "schema": "geohub", + "columns": { + "storymap_id": { + "name": "storymap_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "savedat": { + "name": "savedat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "FK_storymap_TO_storymap_favourite": { + "name": "FK_storymap_TO_storymap_favourite", + "tableFrom": "storymap_favourite", + "tableTo": "storymap", + "schemaTo": "geohub", + "columnsFrom": [ + "storymap_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "storymap_favourite_pkey": { + "name": "storymap_favourite_pkey", + "columns": [ + "storymap_id", + "user_email" + ] + } + }, + "uniqueConstraints": {} + }, + "geohub.dataset_permission": { + "name": "dataset_permission", + "schema": "geohub", + "columns": { + "dataset_id": { + "name": "dataset_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdat": { + "name": "createdat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedat": { + "name": "updatedat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "FK_dataset_TO_dataset_permission": { + "name": "FK_dataset_TO_dataset_permission", + "tableFrom": "dataset_permission", + "tableTo": "dataset", + "schemaTo": "geohub", + "columnsFrom": [ + "dataset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dataset_permission_pkey": { + "name": "dataset_permission_pkey", + "columns": [ + "dataset_id", + "user_email" + ] + } + }, + "uniqueConstraints": {} + }, + "geohub.stac_collection_product": { + "name": "stac_collection_product", + "schema": "geohub", + "columns": { + "stac_id": { + "name": "stac_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "collection_id": { + "name": "collection_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "product_id": { + "name": "product_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "assets": { + "name": "assets", + "type": "varchar[]", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "stac_collection_product_product_id_fkey": { + "name": "stac_collection_product_product_id_fkey", + "tableFrom": "stac_collection_product", + "tableTo": "product", + "schemaTo": "geohub", + "columnsFrom": [ + "product_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "stac_collection_product_pkey": { + "name": "stac_collection_product_pkey", + "columns": [ + "stac_id", + "collection_id", + "product_id" + ] + } + }, + "uniqueConstraints": {} + }, + "geohub.style_permission": { + "name": "style_permission", + "schema": "geohub", + "columns": { + "style_id": { + "name": "style_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdat": { + "name": "createdat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedat": { + "name": "updatedat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "FK_style_TO_style_permission": { + "name": "FK_style_TO_style_permission", + "tableFrom": "style_permission", + "tableTo": "style", + "schemaTo": "geohub", + "columnsFrom": [ + "style_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "style_permission_pkey": { + "name": "style_permission_pkey", + "columns": [ + "style_id", + "user_email" + ] + } + }, + "uniqueConstraints": {} + }, + "geohub.storymap_permission": { + "name": "storymap_permission", + "schema": "geohub", + "columns": { + "storymap_id": { + "name": "storymap_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_email": { + "name": "user_email", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "permission": { + "name": "permission", + "type": "smallint", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "createdat": { + "name": "createdat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedat": { + "name": "updatedat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "FK_storymap_TO_storymap_permission": { + "name": "FK_storymap_TO_storymap_permission", + "tableFrom": "storymap_permission", + "tableTo": "storymap", + "schemaTo": "geohub", + "columnsFrom": [ + "storymap_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "storymap_permission_pkey": { + "name": "storymap_permission_pkey", + "columns": [ + "storymap_id", + "user_email" + ] + } + }, + "uniqueConstraints": {} + }, + "geohub.dataset_defaultstyle": { + "name": "dataset_defaultstyle", + "schema": "geohub", + "columns": { + "dataset_id": { + "name": "dataset_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "layer_id": { + "name": "layer_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "layer_type": { + "name": "layer_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "style": { + "name": "style", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "colormap_name": { + "name": "colormap_name", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "classification_method": { + "name": "classification_method", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_user": { + "name": "created_user", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true + }, + "createdat": { + "name": "createdat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedat": { + "name": "updatedat", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_user": { + "name": "updated_user", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false + }, + "classification_method_2": { + "name": "classification_method_2", + "type": "varchar", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "FK_dataset_TO_dataset_id, layer_type": { + "name": "FK_dataset_TO_dataset_id, layer_type", + "tableFrom": "dataset_defaultstyle", + "tableTo": "dataset", + "schemaTo": "geohub", + "columnsFrom": [ + "dataset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "dataset_defaultstyle_pkey": { + "name": "dataset_defaultstyle_pkey", + "columns": [ + "dataset_id", + "layer_id", + "layer_type" + ] + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": { + "geohub": "geohub" + }, + "sequences": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": { + "stac_collection_product": { + "columns": { + "assets": { + "isArray": true, + "dimensions": 1, + "rawType": "character varying" + } + } + } + } + } +} \ No newline at end of file diff --git a/sites/geohub/drizzle/meta/_journal.json b/sites/geohub/drizzle/meta/_journal.json new file mode 100644 index 000000000..b8ad5206c --- /dev/null +++ b/sites/geohub/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1726151081473, + "tag": "0000_conscious_madelyne_pryor", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/sites/geohub/drizzle/relations.ts b/sites/geohub/drizzle/relations.ts new file mode 100644 index 000000000..992831b53 --- /dev/null +++ b/sites/geohub/drizzle/relations.ts @@ -0,0 +1,142 @@ +import { relations } from 'drizzle-orm/relations'; +import { + datasetInGeohub, + datasetTagInGeohub, + tagInGeohub, + styleInGeohub, + styleFavouriteInGeohub, + datasetFavouriteInGeohub, + storymapInGeohub, + storymapChaptersInGeohub, + storymapChapterInGeohub, + storymapFavouriteInGeohub, + datasetPermissionInGeohub, + productInGeohub, + stacCollectionProductInGeohub, + stylePermissionInGeohub, + storymapPermissionInGeohub, + datasetDefaultstyleInGeohub +} from '../src/lib/server/schema'; + +export const datasetTagInGeohubRelations = relations(datasetTagInGeohub, ({ one }) => ({ + datasetInGeohub: one(datasetInGeohub, { + fields: [datasetTagInGeohub.datasetId], + references: [datasetInGeohub.id] + }), + tagInGeohub: one(tagInGeohub, { + fields: [datasetTagInGeohub.tagId], + references: [tagInGeohub.id] + }) +})); + +export const datasetInGeohubRelations = relations(datasetInGeohub, ({ many }) => ({ + datasetTagInGeohubs: many(datasetTagInGeohub), + datasetFavouriteInGeohubs: many(datasetFavouriteInGeohub), + datasetPermissionInGeohubs: many(datasetPermissionInGeohub), + datasetDefaultstyleInGeohubs: many(datasetDefaultstyleInGeohub) +})); + +export const tagInGeohubRelations = relations(tagInGeohub, ({ many }) => ({ + datasetTagInGeohubs: many(datasetTagInGeohub) +})); + +export const styleFavouriteInGeohubRelations = relations(styleFavouriteInGeohub, ({ one }) => ({ + styleInGeohub: one(styleInGeohub, { + fields: [styleFavouriteInGeohub.styleId], + references: [styleInGeohub.id] + }) +})); + +export const styleInGeohubRelations = relations(styleInGeohub, ({ many }) => ({ + styleFavouriteInGeohubs: many(styleFavouriteInGeohub), + stylePermissionInGeohubs: many(stylePermissionInGeohub) +})); + +export const datasetFavouriteInGeohubRelations = relations(datasetFavouriteInGeohub, ({ one }) => ({ + datasetInGeohub: one(datasetInGeohub, { + fields: [datasetFavouriteInGeohub.datasetId], + references: [datasetInGeohub.id] + }) +})); + +export const storymapChaptersInGeohubRelations = relations(storymapChaptersInGeohub, ({ one }) => ({ + storymapInGeohub: one(storymapInGeohub, { + fields: [storymapChaptersInGeohub.storymapId], + references: [storymapInGeohub.id] + }), + storymapChapterInGeohub: one(storymapChapterInGeohub, { + fields: [storymapChaptersInGeohub.chapterId], + references: [storymapChapterInGeohub.id] + }) +})); + +export const storymapInGeohubRelations = relations(storymapInGeohub, ({ many }) => ({ + storymapChaptersInGeohubs: many(storymapChaptersInGeohub), + storymapFavouriteInGeohubs: many(storymapFavouriteInGeohub), + storymapPermissionInGeohubs: many(storymapPermissionInGeohub) +})); + +export const storymapChapterInGeohubRelations = relations(storymapChapterInGeohub, ({ many }) => ({ + storymapChaptersInGeohubs: many(storymapChaptersInGeohub) +})); + +export const storymapFavouriteInGeohubRelations = relations( + storymapFavouriteInGeohub, + ({ one }) => ({ + storymapInGeohub: one(storymapInGeohub, { + fields: [storymapFavouriteInGeohub.storymapId], + references: [storymapInGeohub.id] + }) + }) +); + +export const datasetPermissionInGeohubRelations = relations( + datasetPermissionInGeohub, + ({ one }) => ({ + datasetInGeohub: one(datasetInGeohub, { + fields: [datasetPermissionInGeohub.datasetId], + references: [datasetInGeohub.id] + }) + }) +); + +export const stacCollectionProductInGeohubRelations = relations( + stacCollectionProductInGeohub, + ({ one }) => ({ + productInGeohub: one(productInGeohub, { + fields: [stacCollectionProductInGeohub.productId], + references: [productInGeohub.id] + }) + }) +); + +export const productInGeohubRelations = relations(productInGeohub, ({ many }) => ({ + stacCollectionProductInGeohubs: many(stacCollectionProductInGeohub) +})); + +export const stylePermissionInGeohubRelations = relations(stylePermissionInGeohub, ({ one }) => ({ + styleInGeohub: one(styleInGeohub, { + fields: [stylePermissionInGeohub.styleId], + references: [styleInGeohub.id] + }) +})); + +export const storymapPermissionInGeohubRelations = relations( + storymapPermissionInGeohub, + ({ one }) => ({ + storymapInGeohub: one(storymapInGeohub, { + fields: [storymapPermissionInGeohub.storymapId], + references: [storymapInGeohub.id] + }) + }) +); + +export const datasetDefaultstyleInGeohubRelations = relations( + datasetDefaultstyleInGeohub, + ({ one }) => ({ + datasetInGeohub: one(datasetInGeohub, { + fields: [datasetDefaultstyleInGeohub.datasetId], + references: [datasetInGeohub.id] + }) + }) +); diff --git a/sites/geohub/package.json b/sites/geohub/package.json index 17e020989..9ea847404 100644 --- a/sites/geohub/package.json +++ b/sites/geohub/package.json @@ -16,7 +16,10 @@ "coverage": "vitest run --coverage --passWithNoTests", "license": "pnpm licenses list --json > static/license.json", "changelog": "copyfiles CHANGELOG.md static", - "copy-sprite": "copyfiles -u 5 'node_modules/@undp-data/style/dist/sprite/**/*' static/api/mapstyle/sprite && copyfiles -u 5 'node_modules/@undp-data/style/dist/sprite-non-sdf/**/*' static/api/mapstyle/sprite-non-sdf" + "copy-sprite": "copyfiles -u 5 'node_modules/@undp-data/style/dist/sprite/**/*' static/api/mapstyle/sprite && copyfiles -u 5 'node_modules/@undp-data/style/dist/sprite-non-sdf/**/*' static/api/mapstyle/sprite-non-sdf", + "drizzle-kit": "drizzle-kit", + "drizzle-kit:generate": "drizzle-kit generate", + "drizzle-kit:migrate": "drizzle-kit push" }, "keywords": [ "geohub", @@ -88,6 +91,7 @@ "d3-format": "^3.1.0", "d3-sankey": "^0.12.3", "dayjs": "^1.11.13", + "drizzle-kit": "^0.24.2", "easymde": "^2.18.0", "eslint": "^9.11.1", "eslint-config-prettier": "^9.1.0", @@ -144,10 +148,11 @@ "@mapbox/vector-tile": "^2.0.3", "@undp-data/style": "^2.2.0", "crypto-js": "^4.2.0", + "drizzle-orm": "^0.33.0", "jwt-decode": "^4.0.0", "pbf": "^4.0.1", - "pg": "^8.13.0", - "pngjs": "^7.0.0" + "pngjs": "^7.0.0", + "postgres": "^3.4.4" }, "type": "module" } diff --git a/sites/geohub/src/lib/server/DatabaseManager.ts b/sites/geohub/src/lib/server/DatabaseManager.ts deleted file mode 100644 index cd0a856a7..000000000 --- a/sites/geohub/src/lib/server/DatabaseManager.ts +++ /dev/null @@ -1,69 +0,0 @@ -import pkg, { type PoolClient } from 'pg'; -const { Pool } = pkg; -import { env } from '$env/dynamic/private'; - -/** - * Class to manage database connection - */ -class DatabaseManager { - private connectionString: string; - private pool = undefined; - private client: PoolClient | undefined = undefined; - - /** - * Constructor - * @param connectionString optional. if not specified, `DATABASE_CONNECTION` variable from process.env will be used - */ - constructor(connectionString?: string) { - this.connectionString = connectionString ?? env.DATABASE_CONNECTION; - } - - /** - * Start database connection - * @returns PoolClient object - */ - public async start() { - this.pool = new Pool({ connectionString: this.connectionString }); - this.client = await this.pool.connect(); - return this.client; - } - - public async end() { - this.client?.release(); - this.pool?.end(); - this.client = undefined; - this.pool = undefined; - } - - /** - * Start database connection and also start transaction - * @returns PoolClient - */ - public async transactionStart() { - if (!this.client) { - this.client = await this.start(); - } - await this.client.query('BEGIN'); - console.info('Transaction started'); - return this.client; - } - - /** - * Rollback transaction - */ - public async transactionRollback() { - await this.client?.query('ROLLBACK'); - console.info('Transaction rollbacked'); - } - - /** - * End transaction and also end database connection - */ - public async transactionEnd() { - await this.client?.query('COMMIT'); - console.info('Transaction ended'); - this.end(); - } -} - -export default DatabaseManager; diff --git a/sites/geohub/src/lib/server/DatasetManager.ts b/sites/geohub/src/lib/server/DatasetManager.ts index 4b16f1899..93c884a3f 100644 --- a/sites/geohub/src/lib/server/DatasetManager.ts +++ b/sites/geohub/src/lib/server/DatasetManager.ts @@ -1,8 +1,10 @@ -import type { PoolClient } from 'pg'; import type { DatasetFeature } from '$lib/types'; -import type TagManager from './TagManager'; +import TagManager from './TagManager'; import { Permission } from '$lib/config/AppConfig'; import { DatasetPermissionManager } from './DatasetPermissionManager'; +import { db, type TransactionSchema } from '$lib/server/db'; +import { datasetFavouriteInGeohub, datasetInGeohub, datasetTagInGeohub } from './schema'; +import { eq, sql } from 'drizzle-orm'; class DatasetManager { private dataset: DatasetFeature; @@ -32,135 +34,121 @@ class DatasetManager { }); } - public async upsert(client: PoolClient) { - console.debug(`started upserting ${this.dataset.properties.id}`); - const rings = this.dataset.geometry.coordinates as [number, number][][]; - const coordinates = rings[0]; - const wkt = `POLYGON(( - ${coordinates[0].join(' ')}, - ${coordinates[1].join(' ')}, - ${coordinates[2].join(' ')}, - ${coordinates[3].join(' ')}, - ${coordinates[4].join(' ')} - ))`; - let query = { - text: ` - INSERT INTO geohub.dataset ( - id, - url, - name, - description, - is_raster, - license, - bounds, - access_level, - createdat, - created_user, - updatedat, - updated_user - ) - values ( - $1, - $2, - $3, - $4, - $5, - $6, - ST_GeomFROMTEXT('${wkt}', 4326), - $7, - $8::timestamptz, - $9, - $10::timestamptz, - $11 - ) - ON CONFLICT (id) - DO - UPDATE - SET - url=$2, - name=$3, - description=$4, - is_raster=$5, - license=$6, - bounds=ST_GeomFROMTEXT('${wkt}', 4326), - access_level=$7, - createdat=$8::timestamptz, - updatedat=$10::timestamptz, - updated_user=$11`, - values: [ - this.dataset.properties.id, - this.dataset.properties.url, - this.dataset.properties.name, - this.dataset.properties.description, - this.dataset.properties.is_raster, - this.dataset.properties.license, - this.dataset.properties.access_level, - this.dataset.properties.createdat, - this.dataset.properties.created_user, - this.dataset.properties.updatedat, - this.dataset.properties.updated_user - ] - }; - await client.query(query); - console.debug(`updated dataset table`); - - query = { - text: `DELETE FROM geohub.dataset_tag WHERE dataset_id=$1`, - values: [this.dataset.properties.id] - }; - await client.query(query); - - if (this.dataset.properties.tags && this.dataset.properties.tags.length > 0) { - const sql = ` - INSERT INTO geohub.dataset_tag (dataset_id, tag_id) values ${this.dataset.properties.tags - .map((t) => `('${this.dataset.properties.id}', ${t.id})`) - .join(',')}`; - await client.query({ text: sql }); - } - console.debug(`updated dataset_tag table`); - - // if it is new data (no permission settings in the table yet), insert user email address as an owner of the dataset. - const dpm = new DatasetPermissionManager( - this.dataset.properties.id, - this.dataset.properties.updated_user - ); - const permissions = await dpm.getAll(client); - if (permissions.length === 0) { - await dpm.register(client, { - dataset_id: this.dataset.properties.id, - user_email: this.dataset.properties.updated_user, - permission: Permission.OWNER - }); - console.debug(`added ${this.dataset.properties.updated_user} as an owner of the dataset`); - } - console.debug(`ended upserting ${this.dataset.properties.id}`); + public async upsert() { + if (!this.dataset) return; + const datasetId = this.dataset.properties.id as string; + await db.transaction(async (tx) => { + if (!this.dataset) return; + + console.debug(`dataset (id=${datasetId}) started registering`); + + const tags: TagManager = new TagManager(); + + this.addTags(tags); + + await tags.insert(tx as TransactionSchema); + console.debug(`${tags.getTags().length} tags were registered into PostGIS.`); + + this.updateTags(tags); + + const rings = this.dataset.geometry?.coordinates as [number, number][][]; + const coordinates = rings[0]; + const wkt = `POLYGON(( + ${coordinates[0].join(' ')}, + ${coordinates[1].join(' ')}, + ${coordinates[2].join(' ')}, + ${coordinates[3].join(' ')}, + ${coordinates[4].join(' ')} + ))`; + + await tx + .insert(datasetInGeohub) + .values({ + id: datasetId, + url: this.dataset.properties.url, + name: this.dataset.properties.name, + description: this.dataset.properties.description, + isRaster: this.dataset.properties.is_raster, + license: this.dataset.properties.license, + bounds: sql.raw(`ST_GeomFROMTEXT('${wkt}', 4326)`), + accessLevel: this.dataset.properties.access_level, + createdat: this.dataset.properties.createdat, + createdUser: this.dataset.properties.created_user + }) + .onConflictDoUpdate({ + target: [datasetInGeohub.id], + set: { + url: this.dataset.properties.url, + name: this.dataset.properties.name, + description: this.dataset.properties.description, + isRaster: this.dataset.properties.is_raster, + license: this.dataset.properties.license, + bounds: sql.raw(`ST_GeomFROMTEXT('${wkt}', 4326)`), + accessLevel: this.dataset.properties.access_level, + updatedat: this.dataset.properties.updatedat, + updatedUser: this.dataset.properties.updated_user + } + }); + + console.debug(`updated dataset table`); + + await tx.delete(datasetTagInGeohub).where(eq(datasetTagInGeohub.datasetId, datasetId)); + console.debug(`deleted dataset_tag table`); + + if (this.dataset.properties.tags && this.dataset.properties.tags.length > 0) { + for (const tag of this.dataset.properties.tags) { + await tx.insert(datasetTagInGeohub).values({ + datasetId: datasetId, + tagId: tag.id + }); + } + console.debug(`updated dataset_tag table`); + } + + // if it is new data (no permission settings in the table yet), insert user email address as an owner of the dataset. + const dpm = new DatasetPermissionManager( + datasetId, + this.dataset.properties.updated_user as string + ); + const permissions = await dpm.getAll(); + if (permissions.length === 0) { + await dpm.register( + { + dataset_id: datasetId, + user_email: this.dataset.properties.updated_user as string, + permission: Permission.OWNER + }, + tx as TransactionSchema + ); + console.debug(`added ${this.dataset.properties.updated_user} as an owner of the dataset`); + } + console.debug(`dataset (id=${datasetId}) was registered into PostGIS.`); + + await tags.cleanup(tx as TransactionSchema); + console.debug(`unused tags were cleaned`); + + console.debug(`dataset (id=${datasetId}) ended registering`); + }); + return this.dataset; } - public async delete(client: PoolClient, datasetId: string) { + public async delete(datasetId: string) { console.debug(`started deleting ${datasetId}`); - const queryDatasetTag = { - text: ` - DELETE FROM geohub.dataset_tag WHERE dataset_id = $1 - `, - values: [datasetId] - }; - await client.query(queryDatasetTag); - console.debug(`deleted it from dataset_tag table`); - - const queryStar = { - text: `DELETE FROM geohub.dataset_favourite WHERE dataset_id = $1`, - values: [datasetId] - }; - await client.query(queryStar); - console.debug(`deleted it from dataset_favourite table`); - - const queryDataset = { - text: `DELETE FROM geohub.dataset WHERE id = $1`, - values: [datasetId] - }; - await client.query(queryDataset); - console.debug(`deleted it from dataset table`); + + await db.transaction(async (tx) => { + await tx.delete(datasetTagInGeohub).where(eq(datasetTagInGeohub.datasetId, datasetId)); + console.debug(`deleted it from dataset_tag table`); + + await tx + .delete(datasetFavouriteInGeohub) + .where(eq(datasetFavouriteInGeohub.datasetId, datasetId)); + console.debug(`deleted it from dataset_favourite table`); + + await tx.delete(datasetInGeohub).where(eq(datasetInGeohub.id, datasetId)); + console.debug(`deleted it from dataset table`); + }); + console.debug(`ended deleting ${datasetId}`); } } diff --git a/sites/geohub/src/lib/server/DatasetPermissionManager.ts b/sites/geohub/src/lib/server/DatasetPermissionManager.ts index 9f1e871ad..c8c633405 100644 --- a/sites/geohub/src/lib/server/DatasetPermissionManager.ts +++ b/sites/geohub/src/lib/server/DatasetPermissionManager.ts @@ -1,6 +1,6 @@ import { Permission } from '$lib/config/AppConfig'; -import type { PoolClient } from 'pg'; -import { UserPermission } from './UserPermission'; +import { UserPermission } from '$lib/server/UserPermission'; +import type { TransactionSchema } from '$lib/server/db'; export interface DatasetPermission { dataset_id: string; @@ -29,58 +29,52 @@ export class DatasetPermissionManager { /** * get permission for signed user - * @param client * @returns 1: READ, 2: Write, 3: Owner, undefined: no permission registered */ - public getBySignedUser = async (client: PoolClient) => { - return await this.userPermission.getBySignedUser(client); + public getBySignedUser = async () => { + return await this.userPermission.getBySignedUser(); }; /** * get permission for target user - * @param client * @param user_email target user_email address * @returns 1: READ, 2: Write, 3: Owner, undefined: no permission registered */ - public getByUser = async (client: PoolClient, user_email: string) => { - return await this.userPermission.getByUser(client, user_email); + public getByUser = async (user_email: string) => { + return await this.userPermission.getByUser(user_email); }; /** * Get all permission info for a dataset - * @param client * @returns DatasetPermission[] */ - public getAll = async (client: PoolClient) => { - return (await this.userPermission.getAll(client)) as DatasetPermission[]; + public getAll = async (tx?: TransactionSchema) => { + return (await this.userPermission.getAll(tx)) as unknown as DatasetPermission[]; }; /** * Register user permission for a dataset - * @param client * @param dataset_permission DatasetPermission object */ - public register = async (client: PoolClient, dataset_permission: DatasetPermission) => { + public register = async (dataset_permission: DatasetPermission, tx?: TransactionSchema) => { const params = JSON.parse(JSON.stringify(dataset_permission)); - await this.userPermission.register(client, params); + await this.userPermission.register(params, tx); }; /** * Update user permission for a dataset - * @param client * @param dataset_permission DatasetPermission object */ - public update = async (client: PoolClient, dataset_permission: DatasetPermission) => { + public update = async (dataset_permission: DatasetPermission, tx?: TransactionSchema) => { const params = JSON.parse(JSON.stringify(dataset_permission)); - await this.userPermission.update(client, params); + await this.userPermission.update(params, tx); }; /** * Delete user permission for a dataset - * @param client * @param user_email user email address to be deleted */ - public delete = async (client: PoolClient, user_email: string) => { - await this.userPermission.delete(client, user_email); + public delete = async (user_email: string, tx?: TransactionSchema) => { + await this.userPermission.delete(user_email, tx); }; } diff --git a/sites/geohub/src/lib/server/Product.ts b/sites/geohub/src/lib/server/Product.ts index 6922803eb..72613415b 100644 --- a/sites/geohub/src/lib/server/Product.ts +++ b/sites/geohub/src/lib/server/Product.ts @@ -3,7 +3,9 @@ * ALLOWED METHODS: GET, POST, PUT, DELETE */ import type { Product } from '$lib/types'; -import type { PoolClient } from 'pg'; +import { db } from '$lib/server/db'; +import { productInGeohub } from '$lib/server/schema'; +import { eq } from 'drizzle-orm'; export class ProductManager { private product: Product; @@ -17,45 +19,48 @@ export class ProductManager { }; } - public async get(client: PoolClient): Promise { - const query = { - text: `SELECT id, label, expression, description FROM geohub.product WHERE id = $1`, - values: [this.product.id] - }; - const res = await client.query(query); - if (res.rowCount === 0) { - return undefined; - } - return res.rows[0] as Product; + public async get(): Promise { + const products = await db + .select({ + id: productInGeohub.id, + description: productInGeohub.description, + expression: productInGeohub.expression, + label: productInGeohub.label + }) + .from(productInGeohub) + .where(eq(productInGeohub.id, this.product.id)); + + return products.length > 0 ? products[0] : (undefined as unknown as Product); } - public async insert(client: PoolClient): Promise { - const query = `INSERT INTO geohub.product (id, description, expression, label) VALUES ($1, $2, $3, $4)`; - const values = [ - this.product.id, - this.product.description, - this.product.expression, - this.product.label - ]; - await client.query(query, values); + public async insert(): Promise { + await db + .insert(productInGeohub) + .values({ + id: this.product.id as string, + description: this.product.description as string, + expression: this.product.expression as string, + label: this.product.label as string + }) + .returning(); + return this.product; } - public async update(client: PoolClient): Promise { - const query = `UPDATE geohub.product SET description=$2, expression=$3, label=$4 WHERE id=$1`; - const values = [ - this.product.id, - this.product.description, - this.product.expression, - this.product.label - ]; - await client.query(query, values); + public async update(): Promise { + await db + .update(productInGeohub) + .set({ + description: this.product.description as string, + expression: this.product.expression as string, + label: this.product.label as string + }) + .where(eq(productInGeohub.id, this.product.id)); + return this.product; } - public async delete(client: PoolClient): Promise { - const query = `DELETE FROM geohub.product WHERE id=$1`; - const values = [this.product.id]; - await client.query(query, values); + public async delete(): Promise { + await db.delete(productInGeohub).where(eq(productInGeohub.id, this.product.id)); } } diff --git a/sites/geohub/src/lib/server/StorymapManager.ts b/sites/geohub/src/lib/server/StorymapManager.ts index 67ca583ad..ab3676dfd 100644 --- a/sites/geohub/src/lib/server/StorymapManager.ts +++ b/sites/geohub/src/lib/server/StorymapManager.ts @@ -1,10 +1,16 @@ -import type { PoolClient } from 'pg'; import type { StoryMapChapter, StoryMapConfig } from '$lib/types'; import { AccessLevel, Permission } from '$lib/config/AppConfig'; import { StorymapPermissionManager } from './StorymapPermissionManager'; import { v4 as uuidv4 } from 'uuid'; import { getDomainFromEmail } from '$lib/helper'; import { env } from '$env/dynamic/private'; +import { eq, SQL, sql } from 'drizzle-orm'; +import { db, type TransactionSchema } from '$lib/server/db'; +import { + storymapChapterInGeohub, + storymapChaptersInGeohub, + storymapInGeohub +} from '$lib/server/schema'; class StorymapManager { private storymap: StoryMapConfig | undefined; @@ -46,21 +52,30 @@ class StorymapManager { this.storymap = storymap; } - private getSelectSql = (is_superuser: boolean, user_email: string, isCount = false) => { - return ` - WITH no_stars as ( - SELECT storymap_id, count(*) as no_stars FROM geohub.storymap_favourite GROUP BY storymap_id - ), - permission as ( - SELECT storymap_id, permission FROM geohub.storymap_permission - WHERE user_email='${user_email}' - ) - SELECT - ${ - isCount - ? 'count(*) as count' - : ` - a.id, + private getSelectSql = ( + is_superuser: boolean, + user_email: string, + isCount = false, + where?: SQL + ) => { + const sqlChunks: SQL[] = []; + sqlChunks.push( + sql.raw(` + WITH no_stars as ( + SELECT storymap_id, count(*) as no_stars FROM geohub.storymap_favourite GROUP BY storymap_id + ), + permission as ( + SELECT storymap_id, permission FROM geohub.storymap_permission + WHERE user_email='${user_email}' + )`) + ); + sqlChunks.push(sql.raw(`SELECT`)); + if (isCount) { + sqlChunks.push(sql.raw(`count(*) as count`)); + } else { + sqlChunks.push( + sql.raw(` + a.id, a.title, a.logo, a.subtitle, @@ -127,49 +142,62 @@ class StorymapManager { c.updated_user ) AS p )) ORDER BY b.sequence)) AS chapters - ` - } - FROM geohub.storymap a - ${ - isCount - ? '' - : ` - LEFT JOIN geohub.storymap_chapters b - ON a.id = b.storymap_id - LEFT JOIN geohub.storymap_chapter c - ON b.chapter_id = c.id - ` + `) + ); } + + sqlChunks.push(sql.raw(`FROM geohub.storymap a`)); + + if (!isCount) { + sqlChunks.push( + sql.raw(` + LEFT JOIN geohub.storymap_chapters b + ON a.id = b.storymap_id + LEFT JOIN geohub.storymap_chapter c + ON b.chapter_id = c.id + `) + ); + } + + sqlChunks.push( + sql.raw(` LEFT JOIN no_stars z ON a.id = z.storymap_id LEFT JOIN permission p - ON a.id = p.storymap_id - {where} - ${ - isCount - ? '' - : ` - GROUP BY - a.id, - a.title, - a.logo, - a.subtitle, - a.byline, - a.footer, - a.template_id, - a.style_id, - a.base_style_id, - a.access_level, - a.show_progress, - a.createdat, - a.created_user, - a.updatedat, - a.updated_user, - no_stars, - permission - ` + ON a.id = p.storymap_id + `) + ); + + if (where) { + sqlChunks.push(where); } - `; + + if (!isCount) { + sqlChunks.push( + sql.raw(` + GROUP BY + a.id, + a.title, + a.logo, + a.subtitle, + a.byline, + a.footer, + a.template_id, + a.style_id, + a.base_style_id, + a.access_level, + a.show_progress, + a.createdat, + a.created_user, + a.updatedat, + a.updated_user, + no_stars, + permission + `) + ); + } + + return sql.join(sqlChunks, sql.raw(' ')); }; private generateStyleUrl = (story: StoryMapConfig) => { @@ -205,12 +233,12 @@ class StorymapManager { user_email: string, mydataOnly: boolean ) { - let domain: string; + let domain = ''; if (user_email) { domain = getDomainFromEmail(user_email); } - const where = ` + const where = sql.raw(` WHERE ( ${accessLevel === AccessLevel.PUBLIC ? `a.access_level = ${AccessLevel.PUBLIC}` : ''} @@ -263,7 +291,7 @@ class StorymapManager { } ) - ${query ? 'AND (to_tsvector(a.title) @@ to_tsquery($1) OR to_tsvector(a.subtitle) @@ to_tsquery($1))' : ''} + ${query ? `AND (to_tsvector(a.title) @@ to_tsquery('${query}') OR to_tsvector(a.subtitle) @@ to_tsquery('${query}'))` : ''} ${ onlyStar && user_email ? ` @@ -279,18 +307,17 @@ class StorymapManager { AND EXISTS (SELECT storymap_id FROM geohub.storymap_permission WHERE storymap_id = a.id AND user_email = '${user_email}' AND permission >= ${Permission.READ} )` : '' } - `; + `); const values: string[] = []; if (query) { values.push(query); } - return { sql: where, values }; + return where; } public async getTotalCount( - client: PoolClient, query: string, accessLevel: AccessLevel, onlyStar: boolean, @@ -298,19 +325,15 @@ AND EXISTS (SELECT storymap_id FROM geohub.storymap_permission WHERE storymap_id user_email: string, mydataOnly: boolean ) { - let sql = this.getSelectSql(is_superuser, user_email, true); - const where = this.getWhereSql(query, accessLevel, onlyStar, user_email, mydataOnly); - sql = sql.replace('{where}', where.sql); - const res = await client.query({ - text: sql, - values: where.values - }); - if (res.rowCount === 0) { + const sql = this.getSelectSql(is_superuser, user_email, true, where); + + const res = await db.execute(sql); + if (res.length === 0) { return 0; } else { - return Number(res.rows[0].count); + return Number(res[0].count); } } @@ -355,7 +378,6 @@ AND EXISTS (SELECT storymap_id FROM geohub.storymap_permission WHERE storymap_id }; public async search( - client: PoolClient, query: string, limit: number, offset: number, @@ -367,23 +389,24 @@ AND EXISTS (SELECT storymap_id FROM geohub.storymap_permission WHERE storymap_id user_email: string, mydataOnly: boolean ) { - let sql = this.getSelectSql(is_superuser, user_email); - const where = this.getWhereSql(query, accessLevel, onlyStar, user_email, mydataOnly); - sql = sql.replace('{where}', where.sql); + const sqlChunks: SQL[] = []; + const mainSql = this.getSelectSql(is_superuser, user_email, false, where); + sqlChunks.push(mainSql); - sql = `${sql} + sqlChunks.push( + sql.raw(` ORDER BY ${sortByColumn} ${sortOrder} LIMIT ${limit} OFFSET ${offset} - `; - const res = await client.query({ - text: sql, - values: where.values - }); - const stories = res.rows as StoryMapConfig[]; + `) + ); + const finalSql: SQL = sql.join(sqlChunks, sql.raw(' ')); + const result = await db.execute(finalSql); + + const stories = result as unknown as StoryMapConfig[]; for (let i = 0; i < stories.length; i++) { stories[i] = this.generateStyleUrl(stories[i]); @@ -393,246 +416,154 @@ AND EXISTS (SELECT storymap_id FROM geohub.storymap_permission WHERE storymap_id return stories; } - public async getById(client: PoolClient, id: string, is_superuser: boolean, user_email?: string) { - let sql = this.getSelectSql(is_superuser, user_email as string); - const where = ` + public async getById(id: string, is_superuser: boolean, user_email?: string) { + const where = sql.raw(` WHERE - a.id = $1 - `; - sql = sql.replace('{where}', where); + a.id = UUID('${id}') + `); + + const mainSql = this.getSelectSql(is_superuser, user_email as string, false, where); - const query = { - text: sql, - values: [id] - }; - const res = await client.query(query); + const res = await db.execute(mainSql); - if (res.rowCount === 0) { + if (res.length === 0) { return undefined; } - let story = res.rows[0] as StoryMapConfig; + let story = res[0] as unknown as StoryMapConfig; story = this.generateStyleUrl(story); story.links = this.createLinks(story); story.chapters = story.chapters.filter((ch) => ch.id !== null); return story; } - public async upsert(client: PoolClient) { + public async upsert() { if (!this.storymap) return; console.debug(`started upserting ${this.storymap.id}`); - // delete existing chapters - let queryDelete = { - text: `DELETE FROM geohub.storymap_chapter - USING geohub.storymap_chapters - WHERE geohub.storymap_chapter.id = geohub.storymap_chapters.chapter_id - AND geohub.storymap_chapters.storymap_id = $1`, - values: [this.storymap.id] - }; - await client.query(queryDelete); - queryDelete = { - text: `DELETE FROM geohub.storymap_chapters WHERE storymap_id = $1`, - values: [this.storymap.id] - }; - await client.query(queryDelete); - - // insert chapters - for (const ch of this.storymap.chapters) { - const chapter = ch as unknown as StoryMapChapter; - // console.log(JSON.stringify(chapter, null, 4)); - - const queryChapter = { - text: ` - INSERT INTO geohub.storymap_chapter ( - id, - title, - description, - image, - card_hidden, - alignment, - map_interactive, - map_navigation_position, - map_animation, - rotate_animation, - spinglobe, - hidden, - center, - zoom, - bearing, - pitch, - style_id, - base_style_id, - on_chapter_enter, - on_chapter_exit, - legend_position, - show_legend, - createdat, - created_user, - updatedat, - updated_user - ) VALUES ( - $1, $2, $3, $4, $5, $6, - $7, $8, $9, $10, $11, $12, - ST_GeomFromText('POINT(${chapter.location.center.join(' ')})', 4326), - $13, $14, $15, $16, $17, $18, - $19, $20, $21, $22, $23, $24, $25 - ) - `, - values: [ - chapter.id, - chapter.title, - chapter.description, - chapter.image, - chapter.cardHidden ?? false, - chapter.alignment, - chapter.mapInteractive, - chapter.mapNavigationPosition, - chapter.mapAnimation, - chapter.rotateAnimation, - chapter.spinGlobe, - chapter.hidden, - chapter.location.zoom, - chapter.location.bearing, - chapter.location.pitch, - chapter.style_id, - chapter.base_style_id, - chapter.onChapterEnter ? JSON.stringify(chapter.onChapterEnter) : undefined, - chapter.onChapterExit ? JSON.stringify(chapter.onChapterExit) : undefined, - chapter.legendPosition ?? 'bottom-left', - chapter.showLegend ?? false, - chapter.createdat, - chapter.created_user, - chapter.updatedat, - chapter.updated_user - ] - }; - - await client.query(queryChapter); - console.debug(`inserted ${chapter.id} into storymap_chapter table`); - } - - // insert storymap - const queryStorymap = { - text: ` - INSERT INTO geohub.storymap ( - id, - title, - logo, - subtitle, - byline, - footer, - template_id, - style_id, - base_style_id, - access_level, - show_progress, - createdat, - created_user - ) - values ( - $1, - $2, - $3, - $4, - $5, - $6, - $7, - $8, - $9, - $10, - $11, - $12, - $13 - ) - ON CONFLICT (id) - DO - UPDATE - SET - title=$2, - logo=$3, - subtitle=$4, - byline=$5, - footer=$6, - template_id=$7, - style_id=$8, - base_style_id=$9, - access_level=$10, - show_progress=$11, - updatedat=$14, - updated_user=$15`, - values: [ - this.storymap.id, - this.storymap.title, - this.storymap.logo, - this.storymap.subtitle, - this.storymap.byline, - this.storymap.footer, - this.storymap.template_id, - this.storymap.style_id, - this.storymap.base_style_id, - this.storymap.access_level, - this.storymap.showProgress, - this.storymap.createdat, - this.storymap.created_user, - this.storymap.updatedat, - this.storymap.updated_user - ] - }; - - // console.log(queryStorymap); - await client.query(queryStorymap); - console.debug(`updated storymap table`); - - for (const ch of this.storymap.chapters) { - // insert storymap_chapters - const sequence = this.storymap.chapters.indexOf(ch) + 1; - const queryChapters = { - text: `INSERT INTO geohub.storymap_chapters - (storymap_id, chapter_id, sequence) - VALUES ($1, $2, $3)`, - values: [this.storymap.id, ch.id, sequence] - }; - await client.query(queryChapters); - } - console.debug(`updated storymap_chapters table`); + await db.transaction(async (tx) => { + if (!this.storymap) return; + // delete existing chapters + await tx.execute(sql` + DELETE FROM ${storymapChapterInGeohub} + USING ${storymapChaptersInGeohub} + WHERE ${storymapChapterInGeohub.id} = ${storymapChaptersInGeohub.chapterId} + AND ${storymapChaptersInGeohub.storymapId} = ${this.storymap.id} + `); + + await tx + .delete(storymapChaptersInGeohub) + .where(eq(storymapChaptersInGeohub.storymapId, this.storymap.id as string)); + + // insert chapters + for (const ch of this.storymap.chapters) { + const chapter = ch as unknown as StoryMapChapter; + + await tx.insert(storymapChapterInGeohub).values({ + id: chapter.id, + title: chapter.title, + description: chapter.description, + image: chapter.image, + cardHidden: chapter.cardHidden ?? false, + alignment: chapter.alignment, + mapInteractive: chapter.mapInteractive, + mapNavigationPosition: chapter.mapNavigationPosition, + mapAnimation: chapter.mapAnimation, + rotateAnimation: chapter.rotateAnimation, + spinGlobe: chapter.spinGlobe, + hidden: chapter.hidden, + center: sql.raw(`ST_GeomFromText('POINT(${chapter.location.center.join(' ')})', 4326)`), + zoom: chapter.location.zoom, + bearing: chapter.location.bearing, + pitch: chapter.location.pitch, + styleId: chapter.style_id, + baseStyleId: chapter.base_style_id, + onChapterEnter: chapter.onChapterEnter, + onChapterExit: chapter.onChapterExit, + legendPosition: chapter.legendPosition ?? 'bottom-left', + showLegend: chapter.showLegend ?? false, + createdat: chapter.createdat, + createdUser: chapter.created_user, + updatedat: chapter.updatedat, + updatedUser: chapter.updated_user + }); + console.debug(`inserted ${chapter.id} into storymap_chapter table`); + } - // if it is new data (no permission settings in the table yet), insert user email address as an owner of the dataset. - const dpm = new StorymapPermissionManager( - this.storymap.id as string, - this.storymap.created_user as string - ); - const permissions = await dpm.getAll(client); - if (permissions.length === 0) { - await dpm.register(client, { - storymap_id: this.storymap.id as string, - user_email: this.storymap.created_user as string, - permission: Permission.OWNER - }); - console.debug(`added ${this.storymap.created_user} as an owner of the storymap`); - } - console.debug(`ended upserting ${this.storymap.id}`); - return this.storymap; + // insert storymap + await tx + .insert(storymapInGeohub) + .values({ + id: this.storymap.id, + title: this.storymap.title, + logo: this.storymap.logo, + subtitle: this.storymap.subtitle, + byline: this.storymap.byline, + footer: this.storymap.footer, + templateId: this.storymap.template_id, + styleId: this.storymap.style_id, + baseStyleId: this.storymap.base_style_id, + accessLevel: this.storymap.access_level, + showProgress: this.storymap.showProgress, + createdat: this.storymap.createdat, + createdUser: this.storymap.created_user + }) + .onConflictDoUpdate({ + target: [storymapInGeohub.id], + set: { + title: this.storymap.title, + logo: this.storymap.logo, + subtitle: this.storymap.subtitle, + byline: this.storymap.byline, + footer: this.storymap.footer, + templateId: this.storymap.template_id, + styleId: this.storymap.style_id, + baseStyleId: this.storymap.base_style_id, + accessLevel: this.storymap.access_level, + showProgress: this.storymap.showProgress, + updatedat: this.storymap.updatedat, + updatedUser: this.storymap.updated_user + } + }); + + console.debug(`updated storymap table`); + + for (const ch of this.storymap.chapters) { + // insert storymap_chapters + const sequence = this.storymap.chapters.indexOf(ch) + 1; + + await tx.insert(storymapChaptersInGeohub).values({ + storymapId: this.storymap.id as string, + chapterId: ch.id, + sequence: sequence + }); + } + console.debug(`updated storymap_chapters table`); + + // if it is new data (no permission settings in the table yet), insert user email address as an owner of the dataset. + const dpm = new StorymapPermissionManager( + this.storymap.id as string, + this.storymap.created_user as string + ); + const permissions = await dpm.getAll(tx as TransactionSchema); + if (permissions.length === 0) { + await dpm.register( + { + storymap_id: this.storymap.id as string, + user_email: this.storymap.created_user as string, + permission: Permission.OWNER + }, + tx as TransactionSchema + ); + console.debug(`added ${this.storymap.created_user} as an owner of the storymap`); + } + console.debug(`ended upserting ${this.storymap.id}`); + return this.storymap; + }); } - public async delete(client: PoolClient, storymapId: string) { + public async delete(storymapId: string) { console.debug(`started deleting ${storymapId}`); - - const queryChapters = { - text: ` - DELETE FROM geohub.storymap_chapter - USING geohub.storymap_chapters - WHERE geohub.storymap_chapters.chapter_id = geohub.storymap_chapter.id - AND geohub.storymap_chapters.storymap_id = $1 - `, - values: [storymapId] - }; - await client.query(queryChapters); - - const queryStorymap = { - text: `DELETE FROM geohub.storymap WHERE id = $1`, - values: [storymapId] - }; - await client.query(queryStorymap); + await db.delete(storymapInGeohub).where(eq(storymapInGeohub.id, storymapId)); console.debug(`ended deleting ${storymapId}`); } } diff --git a/sites/geohub/src/lib/server/StorymapPermissionManager.ts b/sites/geohub/src/lib/server/StorymapPermissionManager.ts index 5a0b8fe88..17a07967c 100644 --- a/sites/geohub/src/lib/server/StorymapPermissionManager.ts +++ b/sites/geohub/src/lib/server/StorymapPermissionManager.ts @@ -1,6 +1,6 @@ import { Permission } from '$lib/config/AppConfig'; -import type { PoolClient } from 'pg'; -import { UserPermission } from './UserPermission'; +import { UserPermission } from '$lib/server/UserPermission'; +import type { TransactionSchema } from '$lib/server/db'; export interface StorymapPermission { storymap_id: string; @@ -29,58 +29,52 @@ export class StorymapPermissionManager { /** * get permission for signed user - * @param client * @returns 1: READ, 2: Write, 3: Owner, undefined: no permission registered */ - public getBySignedUser = async (client: PoolClient) => { - return await this.userPermission.getBySignedUser(client); + public getBySignedUser = async () => { + return await this.userPermission.getBySignedUser(); }; /** * get permission for target user - * @param client * @param user_email target user_email address * @returns 1: READ, 2: Write, 3: Owner, undefined: no permission registered */ - public getByUser = async (client: PoolClient, user_email: string) => { - return await this.userPermission.getByUser(client, user_email); + public getByUser = async (user_email: string) => { + return await this.userPermission.getByUser(user_email); }; /** * Get all permission info for a storymap - * @param client * @returns StorymapPermission[] */ - public getAll = async (client: PoolClient) => { - return (await this.userPermission.getAll(client)) as StorymapPermission[]; + public getAll = async (tx?: TransactionSchema) => { + return (await this.userPermission.getAll(tx)) as unknown as StorymapPermission[]; }; /** * Register user permission for a storymap - * @param client * @param storymap_permission StorymapPermission object */ - public register = async (client: PoolClient, storymap_permission: StorymapPermission) => { + public register = async (storymap_permission: StorymapPermission, tx?: TransactionSchema) => { const params = JSON.parse(JSON.stringify(storymap_permission)); - await this.userPermission.register(client, params); + await this.userPermission.register(params, tx); }; /** * Update user permission for a storymap - * @param client * @param storymap_permission StorymapPermission object */ - public update = async (client: PoolClient, storymap_permission: StorymapPermission) => { + public update = async (storymap_permission: StorymapPermission, tx?: TransactionSchema) => { const params = JSON.parse(JSON.stringify(storymap_permission)); - await this.userPermission.update(client, params); + await this.userPermission.update(params, tx); }; /** * Delete user permission for a storymap - * @param client * @param user_email user email address to be deleted */ - public delete = async (client: PoolClient, user_email: string) => { - await this.userPermission.delete(client, user_email); + public delete = async (user_email: string, tx?: TransactionSchema) => { + await this.userPermission.delete(user_email, tx); }; } diff --git a/sites/geohub/src/lib/server/StylePermissionManager.ts.ts b/sites/geohub/src/lib/server/StylePermissionManager.ts similarity index 58% rename from sites/geohub/src/lib/server/StylePermissionManager.ts.ts rename to sites/geohub/src/lib/server/StylePermissionManager.ts index 0ca5e1e01..f4bae53c6 100644 --- a/sites/geohub/src/lib/server/StylePermissionManager.ts.ts +++ b/sites/geohub/src/lib/server/StylePermissionManager.ts @@ -1,6 +1,6 @@ import { Permission } from '$lib/config/AppConfig'; -import type { PoolClient } from 'pg'; -import { UserPermission } from './UserPermission'; +import { UserPermission } from '$lib/server/UserPermission'; +import type { TransactionSchema } from '$lib/server/db'; export interface StylePermission { style_id: string; @@ -29,58 +29,52 @@ export class StylePermissionManager { /** * get permission for signed user - * @param client * @returns 1: READ, 2: Write, 3: Owner, undefined: no permission registered */ - public getBySignedUser = async (client: PoolClient) => { - return await this.userPermission.getBySignedUser(client); + public getBySignedUser = async () => { + return await this.userPermission.getBySignedUser(); }; /** * get permission for target user - * @param client * @param user_email target user_email address * @returns 1: READ, 2: Write, 3: Owner, undefined: no permission registered */ - public getByUser = async (client: PoolClient, user_email: string) => { - return await this.userPermission.getByUser(client, user_email); + public getByUser = async (user_email: string) => { + return await this.userPermission.getByUser(user_email); }; /** * Get all permission info for a style - * @param client * @returns StylePermission[] */ - public getAll = async (client: PoolClient) => { - return (await this.userPermission.getAll(client)) as StylePermission[]; + public getAll = async (tx?: TransactionSchema) => { + return (await this.userPermission.getAll(tx)) as unknown as StylePermission[]; }; /** * Register user permission for a style - * @param client * @param style_permission StylePermission object */ - public register = async (client: PoolClient, style_permission: StylePermission) => { + public register = async (style_permission: StylePermission, tx?: TransactionSchema) => { const params = JSON.parse(JSON.stringify(style_permission)); - await this.userPermission.register(client, params); + await this.userPermission.register(params, tx); }; /** * Update user permission for a style - * @param client * @param style_permission StylePermission object */ - public update = async (client: PoolClient, style_permission: StylePermission) => { + public update = async (style_permission: StylePermission, tx?: TransactionSchema) => { const params = JSON.parse(JSON.stringify(style_permission)); - await this.userPermission.update(client, params); + await this.userPermission.update(params, tx); }; /** * Delete user permission for a style - * @param client * @param user_email user email address to be deleted */ - public delete = async (client: PoolClient, user_email: string) => { - await this.userPermission.delete(client, user_email); + public delete = async (user_email: string, tx?: TransactionSchema) => { + await this.userPermission.delete(user_email, tx); }; } diff --git a/sites/geohub/src/lib/server/TagManager.ts b/sites/geohub/src/lib/server/TagManager.ts index 5b331a92b..d3273def7 100644 --- a/sites/geohub/src/lib/server/TagManager.ts +++ b/sites/geohub/src/lib/server/TagManager.ts @@ -1,5 +1,7 @@ -import type { PoolClient } from 'pg'; import type { Tag } from '$lib/types'; +import { sql } from 'drizzle-orm'; +import { datasetTagInGeohub, tagInGeohub } from '$lib/server/schema'; +import { db, type TransactionSchema } from '$lib/server/db'; class TagManager { private tags: Tag[]; @@ -21,8 +23,8 @@ class TagManager { } } - public async insert(client: PoolClient) { - const masterTags = await this.load(client); + public async insert(tx?: TransactionSchema) { + const masterTags = await this.load(tx); for (const tag of this.tags) { const masterTag = masterTags.find((t) => { @@ -33,22 +35,16 @@ class TagManager { } }); if (!masterTag) { - let sql = `INSERT INTO geohub.tag (value) values ($1) returning id`; - const values = [tag.value]; - if (tag.key) { - sql = `INSERT INTO geohub.tag (value, key) values ($1, $2) returning id`; - values.push(tag.key); - } - - const query = { - text: sql, - values: values - }; - - const res = await client.query(query); - if (res.rowCount === 0) continue; - const id = res.rows[0].id; - tag.id = id; + const inserted = await (tx ?? db) + .insert(tagInGeohub) + .values({ + value: tag.value as string, + key: tag.key + }) + .returning({ id: tagInGeohub.id }); + if (inserted.length === 0) continue; + const id = inserted[0].id; + tag.id = `${id}`; } else { tag.id = masterTag.id; } @@ -56,32 +52,23 @@ class TagManager { return this.tags; } - private async load(client: PoolClient): Promise { - const query = { - text: `SELECT id, value, key FROM geohub.tag` - }; - const res = await client.query(query); - const tags: Tag[] = []; - if (res.rowCount === 0) return tags; - res.rows.forEach((row) => { - tags.push({ - id: row.id, - value: row.value, - key: row.key - }); - }); + private async load(tx?: TransactionSchema): Promise { + const tags: Tag[] = (await (tx ?? db) + .select({ + id: tagInGeohub.id, + key: tagInGeohub.key, + value: tagInGeohub.value + }) + .from(tagInGeohub)) as unknown as Tag[]; return tags; } - public async cleanup(client: PoolClient) { - const query = { - text: ` - DELETE FROM geohub.tag x - WHERE - (NOT EXISTS (SELECT tag_id FROM geohub.dataset_tag WHERE tag_id = x.id)) - ` - }; - await client.query(query); + public async cleanup(tx?: TransactionSchema) { + await (tx ?? db).execute(sql` + DELETE FROM ${tagInGeohub} + WHERE + (NOT EXISTS (SELECT ${datasetTagInGeohub.tagId} FROM ${datasetTagInGeohub} WHERE ${datasetTagInGeohub.tagId} = ${tagInGeohub.id})) + `); } } diff --git a/sites/geohub/src/lib/server/UserPermission.ts b/sites/geohub/src/lib/server/UserPermission.ts index 957672d19..3575c02d9 100644 --- a/sites/geohub/src/lib/server/UserPermission.ts +++ b/sites/geohub/src/lib/server/UserPermission.ts @@ -1,7 +1,8 @@ import { Permission } from '$lib/config/AppConfig'; import { error } from '@sveltejs/kit'; -import type { PoolClient } from 'pg'; -import { isSuperuser } from './helpers'; +import { isSuperuser } from '$lib/server/helpers'; +import { db, type TransactionSchema } from '$lib/server/db'; +import { sql } from 'drizzle-orm'; export class UserPermission { private id: string; @@ -26,51 +27,52 @@ export class UserPermission { /** * get permission for signed user - * @param client * @returns 1: READ, 2: Write, 3: Owner, undefined: no permission registered */ - public getBySignedUser = async (client: PoolClient) => { - return await this.getByUser(client, this.signed_user); + public getBySignedUser = async () => { + return await this.getByUser(this.signed_user); }; /** * get permission for target user - * @param client * @param user_email target user_email address * @returns 1: READ, 2: Write, 3: Owner, undefined: no permission registered */ - public getByUser = async (client: PoolClient, user_email: string) => { - const query = { - text: `SELECT permission FROM geohub.${this.TABLE_NAME} WHERE ${this.ID_COLUMN_NAME}=$1 and user_email = $2`, - values: [this.id, user_email] - }; - const res = await client.query(query); - if (res.rowCount === 0) { + public getByUser = async (user_email: string) => { + const res = await db.execute( + sql.raw(` + SELECT permission + FROM geohub.${this.TABLE_NAME} + WHERE ${this.ID_COLUMN_NAME}='${this.id}' + and + user_email = '${user_email}' + `) + ); + + if (res.length === 0) { return undefined; } - const permission = res.rows[0]; + const permission = res[0]; return permission.permission; }; /** * Get all permission info for a dataset - * @param client * @returns DatasetPermission[] */ - public getAll = async (client: PoolClient) => { - const query = { - text: ` - SELECT ${this.ID_COLUMN_NAME}, user_email, permission, createdat, updatedat + public getAll = async (tx?: TransactionSchema) => { + const res = await (tx ?? db).execute( + sql.raw(` + SELECT ${this.ID_COLUMN_NAME}, user_email, permission, createdat, updatedat FROM geohub.${this.TABLE_NAME} - WHERE ${this.ID_COLUMN_NAME}=$1 - `, - values: [this.id] - }; - const res = await client.query(query); - return res.rows; + WHERE ${this.ID_COLUMN_NAME}='${this.id}' + `) + ); + + return res; }; - private upsert = async (client: PoolClient, user_permission: { [key: string]: string }) => { + private upsert = async (user_permission: { [key: string]: string }, tx?: TransactionSchema) => { const now = new Date().toISOString(); if (!user_permission.createdat) { user_permission.createdat = now; @@ -79,8 +81,8 @@ export class UserPermission { user_permission.updatedat = now; } - const query = { - text: ` + await (tx ?? db).execute( + sql.raw(` INSERT INTO geohub.${this.TABLE_NAME} ( ${this.ID_COLUMN_NAME}, user_email, @@ -88,44 +90,30 @@ export class UserPermission { createdat ) values ( - $1, - $2, - $3, - $4::timestamptz + '${user_permission[this.ID_COLUMN_NAME]}', + '${user_permission.user_email}', + ${user_permission.permission}, + '${user_permission.createdat}'::timestamptz ) ON CONFLICT (${this.ID_COLUMN_NAME}, user_email) DO UPDATE SET - permission=$3, - updatedat=$5::timestamptz - `, - values: [ - user_permission[this.ID_COLUMN_NAME], - user_permission.user_email, - user_permission.permission, - user_permission.createdat, - user_permission.updatedat - ] - }; - try { - await client.query(query); - } catch (err) { - client.query('ROLLBACK'); - error(500, err); - } + permission=${user_permission.permission}, + updatedat='${user_permission.updatedat}'::timestamptz + `) + ); }; /** * Register user permission - * @param client * @param user_permission user_permission info */ - public register = async (client: PoolClient, user_permission: { [key: string]: string }) => { + public register = async (user_permission: { [key: string]: string }, tx?: TransactionSchema) => { const is_superuser = await isSuperuser(this.signed_user); - const permissions = await this.getAll(client); + const permissions = await this.getAll(); if (!is_superuser) { - const signedUserPermission = await this.getBySignedUser(client); + const signedUserPermission = await this.getBySignedUser(); // no permission is registered yet, it allows to register if (!(permissions.length === 0 && !signedUserPermission)) { @@ -149,15 +137,14 @@ export class UserPermission { } } - await this.upsert(client, user_permission); + await this.upsert(user_permission, tx); }; /** * Update user permission - * @param client * @param user_permission user_permission info */ - public update = async (client: PoolClient, user_permission: { [key: string]: string }) => { + public update = async (user_permission: { [key: string]: string }, tx?: TransactionSchema) => { const is_superuser = await isSuperuser(this.signed_user); if (!is_superuser) { // cannot delete signed in user themselves @@ -166,7 +153,7 @@ export class UserPermission { } // only users with owner/write/read permission can update. - const signedUserPermission = await this.getBySignedUser(client); + const signedUserPermission = await this.getBySignedUser(); if (!(signedUserPermission && signedUserPermission >= Permission.READ)) { error(403, { message: `You have no permission to register this user's permission.` }); } @@ -177,7 +164,7 @@ export class UserPermission { } } - const permissions = await this.getAll(client); + const permissions = await this.getAll(); if (permissions.length > 0) { // if target user is not registered to the table if (!permissions.find((p) => p.user_email === user_permission.user_email)) { @@ -187,15 +174,14 @@ export class UserPermission { } } - await this.upsert(client, user_permission); + await this.upsert(user_permission, tx); }; /** * Delete user permission - * @param client * @param user_email user email address to be deleted */ - public delete = async (client: PoolClient, user_email: string) => { + public delete = async (user_email: string, tx?: TransactionSchema) => { const is_superuser = await isSuperuser(this.signed_user); if (!is_superuser) { // cannot delete signed in user themselves @@ -204,19 +190,19 @@ export class UserPermission { } // only users with owner/write/read permission can delete. - const permission = await this.getBySignedUser(client); + const permission = await this.getBySignedUser(); if (!(permission && permission >= Permission.READ)) { error(403, { message: `You have no permission to delete this user's permission.` }); } // users users cannot delete permission which is higher than their own permission to a user. - const targetPermission = await this.getByUser(client, user_email); + const targetPermission = await this.getByUser(user_email); if (permission < Permission.OWNER && targetPermission === Permission.OWNER) { error(403, { message: `You have no permission to delete this user's permission.` }); } } - const permissions = await this.getAll(client); + const permissions = await this.getAll(tx); if (permissions.length > 0) { // if target user is not registered to the table if (!permissions.find((p) => p.user_email === user_email)) { @@ -234,18 +220,11 @@ export class UserPermission { } } - const query = { - text: ` - DELETE FROM geohub.${this.TABLE_NAME} - WHERE ${this.ID_COLUMN_NAME}=$1 AND user_email =$2 - `, - values: [this.id, user_email] - }; - try { - await client.query(query); - } catch (err) { - client.query('ROLLBACK'); - error(500, err); - } + await (tx ?? db).execute( + sql.raw(` + DELETE FROM geohub.${this.TABLE_NAME} + WHERE ${this.ID_COLUMN_NAME}='${this.id}' AND user_email='${user_email}' + `) + ); }; } diff --git a/sites/geohub/src/lib/server/db.ts b/sites/geohub/src/lib/server/db.ts new file mode 100644 index 000000000..a28ab1c15 --- /dev/null +++ b/sites/geohub/src/lib/server/db.ts @@ -0,0 +1,13 @@ +import { drizzle, type PostgresJsQueryResultHKT } from 'drizzle-orm/postgres-js'; +import postgres from 'postgres'; +import { env } from '$env/dynamic/private'; +import * as schema from '$lib/server/schema.js'; +import { PgTransaction } from 'drizzle-orm/pg-core'; + +const queryClient = postgres(env.DATABASE_CONNECTION); +export const db = drizzle(queryClient, { schema }); + +export type TransactionSchema = PgTransaction< + PostgresJsQueryResultHKT, + typeof import('$lib/server/schema') +>; diff --git a/sites/geohub/src/lib/server/helpers/createDatasetSearchWhereExpression.ts b/sites/geohub/src/lib/server/helpers/createDatasetSearchWhereExpression.ts index 3299f7111..9cd1432da 100644 --- a/sites/geohub/src/lib/server/helpers/createDatasetSearchWhereExpression.ts +++ b/sites/geohub/src/lib/server/helpers/createDatasetSearchWhereExpression.ts @@ -1,5 +1,6 @@ import { AccessLevel, DatasetSearchQueryParams, Permission } from '$lib/config/AppConfig'; import { getDomainFromEmail } from '$lib/helper'; +import { SQL, sql } from 'drizzle-orm'; export const createDatasetSearchWhereExpression = async ( url: URL, @@ -13,7 +14,7 @@ export const createDatasetSearchWhereExpression = async ( queryOperator = 'and'; } const bbox = url.searchParams.get('bbox'); - let bboxCoordinates: number[]; + let bboxCoordinates: number[] = []; if (bbox) { bboxCoordinates = bbox.split(',').map((val) => Number(val)); if (bboxCoordinates.length !== 4) { @@ -34,14 +35,13 @@ export const createDatasetSearchWhereExpression = async ( if (DatasetSearchQueryParams.includes(value)) return; filters.push({ key: value, - value: key.toLowerCase() + value: key.toLowerCase().replace(/\s+/g, ` & `) }); }); const _onlyStar = url.searchParams.get('staronly') || 'false'; const onlyStar = _onlyStar.toLowerCase() === 'true'; - const values = []; if (query) { // normalise query text for to_tsquery function queryOperator = queryOperator.trim().toLowerCase(); @@ -55,7 +55,6 @@ export const createDatasetSearchWhereExpression = async ( query = query.slice(0, query.length - 2); query = query.trim(); } - values.push(query); } const mydata = url.searchParams.get('mydata'); @@ -63,7 +62,10 @@ export const createDatasetSearchWhereExpression = async ( const domain = user_email ? getDomainFromEmail(user_email) : undefined; - const sql = ` + const sqlChunks: SQL[] = []; + + sqlChunks.push( + sql.raw(` WHERE NOT ST_IsEmpty(${tableAlias}.bounds) ${ @@ -71,16 +73,22 @@ export const createDatasetSearchWhereExpression = async ( ? '' : ` AND ( - to_tsvector(${tableAlias}.name) @@ to_tsquery($1) - OR to_tsvector(${tableAlias}.description) @@ to_tsquery($1) + to_tsvector(${tableAlias}.name) @@ to_tsquery('${query}') + OR to_tsvector(${tableAlias}.description) @@ to_tsquery('${query}') )` } - ${ - operator === 'and' - ? getTagFilterAND(filters, values, tableAlias) - : getTagFilterOR(filters, values, tableAlias) - } - ${getBBoxFilter(bboxCoordinates, values, tableAlias)} + `) + ); + if (operator === 'and') { + sqlChunks.push(getTagFilterAND(filters, tableAlias)); + } else { + sqlChunks.push(getTagFilterOR(filters, tableAlias)); + } + + sqlChunks.push(getBBoxFilter(bboxCoordinates, tableAlias)); + + sqlChunks.push( + sql.raw(` ${ onlyStar && user_email ? ` @@ -117,21 +125,15 @@ export const createDatasetSearchWhereExpression = async ( ) ` } - `; + `) + ); - return { - sql, - values - }; + return sql.join(sqlChunks, sql.raw(' ')); }; -const getTagFilterOR = ( - filters: { key?: string; value: string }[], - values: string[], - tableAlias: string -) => { - if (filters.length === 0) return ''; - return ` +const getTagFilterOR = (filters: { key?: string; value: string }[], tableAlias: string) => { + if (filters.length === 0) return sql.raw(''); + return sql.raw(` AND EXISTS( SELECT a.id FROM geohub.tag as a @@ -140,60 +142,46 @@ const getTagFilterOR = ( WHERE b.dataset_id = ${tableAlias}.id AND ( ${filters .map((filter) => { - values.push(filter.key); - const keyLength = values.length; - values.push(`'${filter.value}'`); - const valueLength = values.length; - return `(a.key = $${keyLength} and to_tsvector(a.value) @@ to_tsquery($${valueLength})) `; + return `(a.key = '${filter.key}' and to_tsvector(a.value) @@ to_tsquery('${filter.value}')) `; }) .join('OR')} - ))`; + ))`); }; -const getTagFilterAND = ( - filters: { key?: string; value: string }[], - values: string[], - tableAlias: string -) => { - if (filters.length === 0) return ''; - return ` +const getTagFilterAND = (filters: { key?: string; value: string }[], tableAlias: string) => { + if (filters.length === 0) return sql.raw(''); + + return sql.raw(` AND EXISTS( SELECT dataset_id FROM ( ${filters .map((filter) => { - values.push(filter.key); - const keyLength = values.length; - values.push(`'${filter.value}'`); - const valueLength = values.length; return ` SELECT b.dataset_id FROM geohub.tag as a INNER JOIN geohub.dataset_tag as b ON a.id = b.tag_id - WHERE a.key =$${keyLength} and to_tsvector(a.value) @@ to_tsquery($${valueLength}) + WHERE a.key = '${filter.key}' and to_tsvector(a.value) @@ to_tsquery('${filter.value}') `; }) .join('INTERSECT')} ) y WHERE dataset_id = ${tableAlias}.id - )`; + )`); }; -const getBBoxFilter = (bbox: number[], values: string[], tableAlias: string) => { - if (!(bbox && bbox.length === 4)) return ''; - bbox.forEach((val) => { - values.push(val.toString()); - }); - return ` +const getBBoxFilter = (bbox: number[], tableAlias: string) => { + if (!(bbox && bbox.length === 4)) return sql.raw(''); + return sql.raw(` AND ST_INTERSECTS( ${tableAlias}.bounds, ST_MakeEnvelope( - $${values.length - 3}::double precision, - $${values.length - 2}::double precision, - $${values.length - 1}::double precision, - $${values.length}::double precision + ${bbox[0]}::double precision, + ${bbox[1]}::double precision, + ${bbox[2]}::double precision, + ${bbox[3]}::double precision , 4326 ) ) - `; + `); }; diff --git a/sites/geohub/src/lib/server/helpers/getDatasetById.ts b/sites/geohub/src/lib/server/helpers/getDatasetById.ts index b06883060..983d43dea 100644 --- a/sites/geohub/src/lib/server/helpers/getDatasetById.ts +++ b/sites/geohub/src/lib/server/helpers/getDatasetById.ts @@ -1,16 +1,11 @@ import { Permission } from '$lib/config/AppConfig'; import { generateAzureBlobSasToken } from '$lib/server/helpers'; import type { DatasetFeature, Tag } from '$lib/types'; -import type { PoolClient } from 'pg'; +import { sql } from 'drizzle-orm'; +import { db } from '$lib/server/db'; -export const getDatasetById = async ( - client: PoolClient, - id: string, - is_superuser: boolean, - user_email?: string -) => { - const sql = { - text: ` +export const getDatasetById = async (id: string, is_superuser: boolean, user_email?: string) => { + const query = sql.raw(` WITH datasetTags as ( SELECT x.id, @@ -98,17 +93,18 @@ export const getDatasetById = async ( LEFT JOIN no_stars z ON x.id = z.dataset_id ${!is_superuser && user_email ? `LEFT JOIN permission p ON x.id = p.dataset_id` : ''} - WHERE x.id=$1 + WHERE x.id='${id}' ) AS feature - `, - values: [id] - }; + `); + + const data = await db.execute(query); + // console.log(sql); - const res = await client.query(sql); - if (res.rowCount === 0) { + // const res = await client.query(sql); + if (data.length === 0) { return; } - const feature: DatasetFeature = res.rows[0].feature; + const feature: DatasetFeature = data[0].feature as unknown as DatasetFeature; // add SAS token if it is Azure Blob source const tags: Tag[] = feature.properties.tags; const type = tags?.find((tag) => tag.key === 'type'); diff --git a/sites/geohub/src/lib/server/helpers/getDatasetStarCount.ts b/sites/geohub/src/lib/server/helpers/getDatasetStarCount.ts index 12deb7709..a91fd338b 100644 --- a/sites/geohub/src/lib/server/helpers/getDatasetStarCount.ts +++ b/sites/geohub/src/lib/server/helpers/getDatasetStarCount.ts @@ -1,21 +1,13 @@ -import type { PoolClient } from 'pg'; +import { db } from '$lib/server/db'; +import { datasetFavouriteInGeohub } from '../schema'; +import { count, eq } from 'drizzle-orm'; -export const getDatasetStarCount = async (client: PoolClient, dataset_id: string) => { - const query = { - text: ` - SELECT count(*) as stars - FROM geohub.dataset_favourite - WHERE dataset_id = $1 - GROUP BY dataset_id - `, - values: [dataset_id] - }; +export const getDatasetStarCount = async (dataset_id: string) => { + const result = await db + .select({ count: count() }) + .from(datasetFavouriteInGeohub) + .where(eq(datasetFavouriteInGeohub.datasetId, dataset_id)) + .groupBy(datasetFavouriteInGeohub.datasetId); - const res = await client.query(query); - - if (res.rowCount === 0) { - return 0; - } else { - return res.rows[0]['stars']; - } + return result.length === 0 ? 0 : result[0].count; }; diff --git a/sites/geohub/src/lib/server/helpers/getDatasetStats.ts b/sites/geohub/src/lib/server/helpers/getDatasetStats.ts index 0763be1d5..82165e8a4 100644 --- a/sites/geohub/src/lib/server/helpers/getDatasetStats.ts +++ b/sites/geohub/src/lib/server/helpers/getDatasetStats.ts @@ -1,86 +1,79 @@ -import DatabaseManager from '$lib/server/DatabaseManager'; import type { StatsCard } from '@undp-data/svelte-undp-design'; import { AccessLevel } from '$lib/config/AppConfig'; +import { db } from '$lib/server/db'; +import { sql } from 'drizzle-orm'; +import { datasetInGeohub, datasetTagInGeohub, tagInGeohub } from '../schema'; export const getDatasetStats = async () => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: ` - WITH public_datasets AS( - SELECT a.id as dataset_id, c.id as tag_id, c.key, c.value - FROM geohub.dataset a - LEFT JOIN geohub.dataset_tag b - ON a.id = b.dataset_id - LEFT JOIN geohub.tag c - ON b.tag_id = c.id - WHERE access_level=${AccessLevel.PUBLIC} - ), - tag_count AS ( - SELECT z.key, z.value, COUNT(x.id) as count - FROM geohub.dataset x - INNER JOIN geohub.dataset_tag y - ON x.id = y.dataset_id - INNER JOIN geohub.tag z - ON y.tag_id = z.id - GROUP BY - z.key, z.value - ) - SELECT * FROM( - SELECT - 1 as id, - 'Public datasets' as title, - 'The number of public datasets' as description, - count(x.*) as count - FROM (SELECT dataset_id FROM public_datasets GROUP BY dataset_id) x - UNION - SELECT - 2 as id, - 'Global datasets' as title, - 'The number of public global datasets' as description, - count(x.*) as count - FROM (SELECT dataset_id FROM public_datasets WHERE value='Global' GROUP BY dataset_id) x - UNION - SELECT - 3 as id, - 'SDG datasets' as title, - 'We have datasets across all SDGs' as description, - count(x.*) as count - FROM (SELECT dataset_id FROM public_datasets WHERE key='sdg_goal' GROUP BY dataset_id) x - UNION - SELECT - 4 as id, - 'Country datasets' as title, - 'The number of public datasets linked to at least a country' as description, - count(x.*) as count - FROM (SELECT dataset_id FROM public_datasets WHERE key='country' GROUP BY dataset_id) x - UNION - SELECT - 5 as id, - 'Countries' as title, - 'The number of countries having GeoHub datasets' as description, - count(x.*) as count - FROM (SELECT value FROM tag_count WHERE key='country') x - ) a - ORDER BY a.id - `, - values: [] - }; - const res = await client.query(query); + const result = await db.execute(sql` + WITH public_datasets AS( + SELECT${datasetInGeohub.id} as dataset_id, ${tagInGeohub.id} as tag_id,${tagInGeohub.key}, ${tagInGeohub.value} + FROM ${datasetInGeohub} + LEFT JOIN ${datasetTagInGeohub} + ON ${datasetInGeohub.id} = ${datasetTagInGeohub.datasetId} + LEFT JOIN ${tagInGeohub} + ON ${datasetTagInGeohub.tagId} = ${tagInGeohub.id} + WHERE ${datasetInGeohub.accessLevel}=${AccessLevel.PUBLIC} + ), + tag_count AS ( + SELECT ${tagInGeohub.key}, ${tagInGeohub.value}, COUNT(${datasetInGeohub.id}) as count + FROM ${datasetInGeohub} + INNER JOIN ${datasetTagInGeohub} + ON ${datasetInGeohub.id} = ${datasetTagInGeohub.datasetId} + INNER JOIN ${tagInGeohub} + ON ${datasetTagInGeohub.tagId} = ${tagInGeohub.id} + GROUP BY + ${tagInGeohub.key}, ${tagInGeohub.value} + ) + SELECT * FROM( + SELECT + 1 as id, + 'Public datasets' as title, + 'The number of public datasets' as description, + count(x.*) as count + FROM (SELECT dataset_id FROM public_datasets GROUP BY dataset_id) x + UNION + SELECT + 2 as id, + 'Global datasets' as title, + 'The number of public global datasets' as description, + count(x.*) as count + FROM (SELECT dataset_id FROM public_datasets WHERE value='Global' GROUP BY dataset_id) x + UNION + SELECT + 3 as id, + 'SDG datasets' as title, + 'We have datasets across all SDGs' as description, + count(x.*) as count + FROM (SELECT dataset_id FROM public_datasets WHERE key='sdg_goal' GROUP BY dataset_id) x + UNION + SELECT + 4 as id, + 'Country datasets' as title, + 'The number of public datasets linked to at least a country' as description, + count(x.*) as count + FROM (SELECT dataset_id FROM public_datasets WHERE key='country' GROUP BY dataset_id) x + UNION + SELECT + 5 as id, + 'Countries' as title, + 'The number of countries having GeoHub datasets' as description, + count(x.*) as count + FROM (SELECT value FROM tag_count WHERE key='country') x + ) a + ORDER BY a.id + `); - const cards: StatsCard[] = []; + const cards: StatsCard[] = []; - res.rows.forEach((row) => { - cards.push({ - stat: row.count, - title: row.title, - description: row.description - }); + for (const row of result) { + const data = row as { id: string; title: string; description: string; count: string }; + cards.push({ + stat: Number(data.count), + title: data.title, + description: data.description }); - - return cards; - } finally { - dbm.end(); } + + return cards; }; diff --git a/sites/geohub/src/lib/server/helpers/getDefaultLayerStyle.ts b/sites/geohub/src/lib/server/helpers/getDefaultLayerStyle.ts index 3cfe1969c..2ee70da59 100644 --- a/sites/geohub/src/lib/server/helpers/getDefaultLayerStyle.ts +++ b/sites/geohub/src/lib/server/helpers/getDefaultLayerStyle.ts @@ -1,40 +1,39 @@ import type { DatasetDefaultLayerStyle } from '$lib/types/DatasetDeaultLayerStyle'; -import type { PoolClient } from 'pg'; +import { db } from '$lib/server/db'; +import { datasetDefaultstyleInGeohub } from '$lib/server/schema'; +import { sql } from 'drizzle-orm'; export const getDefaultLayerStyle = async ( - client: PoolClient, dataset_id: string, layer_id: string, layer_type: string ) => { - const query = { - text: ` - SELECT - dataset_id, - layer_id, - layer_type, - source, - style, - colormap_name, - classification_method, - classification_method_2, - created_user, - createdat, - updatedat, - updated_user - FROM geohub.dataset_defaultstyle - WHERE - dataset_id=$1 - AND layer_id=$2 - AND layer_type=$3 - `, - values: [dataset_id, layer_id, layer_type] - }; + const data = await db + .select({ + dataset_id: datasetDefaultstyleInGeohub.datasetId, + layer_id: datasetDefaultstyleInGeohub.layerId, + layer_type: datasetDefaultstyleInGeohub.layerType, + source: datasetDefaultstyleInGeohub.source, + style: datasetDefaultstyleInGeohub.style, + colormap_name: datasetDefaultstyleInGeohub.colormapName, + classification_method: datasetDefaultstyleInGeohub.classificationMethod, + classification_method_2: datasetDefaultstyleInGeohub.classificationMethod2, + created_user: datasetDefaultstyleInGeohub.createdUser, + createdat: datasetDefaultstyleInGeohub.createdat, + updatedat: datasetDefaultstyleInGeohub.updatedat, + updated_user: datasetDefaultstyleInGeohub.updatedUser + }) + .from(datasetDefaultstyleInGeohub) + .where( + sql` + ${datasetDefaultstyleInGeohub.datasetId} = ${dataset_id} + AND ${datasetDefaultstyleInGeohub.layerId} = ${layer_id} + AND ${datasetDefaultstyleInGeohub.layerType} = ${layer_type} + ` + ); - const res = await client.query(query); - if (res.rowCount === 0) { + if (data.length === 0) { return; } - const data: DatasetDefaultLayerStyle = res.rows[0]; - return data; + return data[0] as DatasetDefaultLayerStyle; }; diff --git a/sites/geohub/src/lib/server/helpers/getMapStats.ts b/sites/geohub/src/lib/server/helpers/getMapStats.ts deleted file mode 100644 index 861015e21..000000000 --- a/sites/geohub/src/lib/server/helpers/getMapStats.ts +++ /dev/null @@ -1,85 +0,0 @@ -import type { PoolClient } from 'pg'; -import type { StatsCard } from '@undp-data/svelte-undp-design'; -import { AccessLevel } from '$lib/config/AppConfig'; -import DatabaseManager from '$lib/server/DatabaseManager'; - -/** - * Get the total count of styles stored in database - * GET: ./api/style/count - */ -export const getMapStats = async () => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: ` - SELECT - access_level, - count(*) as count - FROM geohub.style - GROUP BY access_level - ORDER BY access_level desc - `, - values: [] - }; - - const res = await client.query(query); - - const cards: StatsCard[] = []; - - res.rows.forEach((row) => { - const stat = row.count; - let title = ''; - let description = ''; - if (row.access_level === AccessLevel.PRIVATE) { - title = 'Private Maps'; - description = `${stat} maps created privately`; - } else if (row.access_level === AccessLevel.ORGANIZATION) { - title = 'Maps shared in UNDP'; - description = `${stat} maps created and shared within UNDP`; - } else { - title = 'Public Maps created'; - description = `${stat} maps shared by community`; - } - - cards.push({ - stat, - title, - description - }); - }); - - const userstats = await getUserStats(client); - cards.push(userstats); - return cards; - } finally { - dbm.end(); - } -}; - -const getUserStats = async (client: PoolClient) => { - const query = { - text: ` - WITH userstats AS ( - SELECT - created_user, - count(*) as count - FROM geohub.style - WHERE created_user is not null - GROUP BY created_user - ) - SELECT count(*) as count FROM userstats - `, - values: [] - }; - - const res = await client.query(query); - const stat: number = res.rows[0].count; - const card: StatsCard = { - stat: stat, - title: 'Users', - description: `${stat} users created maps` - }; - - return card; -}; diff --git a/sites/geohub/src/lib/server/helpers/getStoryStarCount.ts b/sites/geohub/src/lib/server/helpers/getStoryStarCount.ts index 565e5c305..33e2c0243 100644 --- a/sites/geohub/src/lib/server/helpers/getStoryStarCount.ts +++ b/sites/geohub/src/lib/server/helpers/getStoryStarCount.ts @@ -1,21 +1,13 @@ -import type { PoolClient } from 'pg'; +import { storymapFavouriteInGeohub } from '../schema'; +import { count, eq } from 'drizzle-orm'; +import { db } from '$lib/server/db'; -export const getStoryStarCount = async (client: PoolClient, storymap_id: string) => { - const query = { - text: ` - SELECT count(*) as stars - FROM geohub.storymap_favourite - WHERE storymap_id = $1 - GROUP BY storymap_id - `, - values: [storymap_id] - }; +export const getStoryStarCount = async (storymap_id: string) => { + const result = await db + .select({ count: count() }) + .from(storymapFavouriteInGeohub) + .where(eq(storymapFavouriteInGeohub.storymapId, storymap_id)) + .groupBy(storymapFavouriteInGeohub.storymapId); - const res = await client.query(query); - - if (res.rowCount === 0) { - return 0; - } else { - return res.rows[0]['stars']; - } + return result.length === 0 ? 0 : result[0].count; }; diff --git a/sites/geohub/src/lib/server/helpers/getStyleById.ts b/sites/geohub/src/lib/server/helpers/getStyleById.ts index 6cc1608e1..44ba424bd 100644 --- a/sites/geohub/src/lib/server/helpers/getStyleById.ts +++ b/sites/geohub/src/lib/server/helpers/getStyleById.ts @@ -1,4 +1,3 @@ -import DatabaseManager from '$lib/server/DatabaseManager'; import type { DashboardMapStyle, VectorLayerSpecification } from '$lib/types'; import { createStyleLinks } from './createStyleLinks'; import { getDatasetById } from './getDatasetById'; @@ -18,19 +17,17 @@ import { createDatasetLinks } from './createDatasetLinks'; import { createAttributionFromTags, getBase64EncodedUrl, getFirstSymbolLayerId } from '$lib/helper'; import { Permission } from '$lib/config/AppConfig'; import { getSTAC, resolveSpriteUrl } from '.'; - import voyagerStyle from '@undp-data/style/dist/style.json'; import darkStyle from '@undp-data/style/dist/dark.json'; import positronStyle from '@undp-data/style/dist/positron.json'; import aerialStyle from '@undp-data/style/dist/aerialstyle.json'; import { DefaultUserConfig } from '$lib/config/DefaultUserConfig'; +import { db } from '$lib/server/db'; +import { sql } from 'drizzle-orm'; export const getStyleById = async (id: number, url: URL, email?: string, is_superuser = false) => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: ` + const data = await db.execute( + sql.raw(` SELECT x.id, x.name, @@ -66,169 +63,171 @@ export const getStyleById = async (id: number, url: URL, email?: string, is_supe FROM geohub.style x LEFT JOIN geohub.style_permission p ON x.id = p.style_id - AND p.user_email = $2 - where id = $1 - `, - values: [id, email] - }; - - const res = await client.query(query); + AND p.user_email = '${email}' + where x.id = ${id} + `) + ); - if (res.rowCount === 0) { - return undefined; - } + if (data.length === 0) { + return undefined; + } - const style: DashboardMapStyle = res.rows[0]; + const style: DashboardMapStyle = data[0] as unknown as DashboardMapStyle; - // set URL origin if URL starts with /api - // if origin is localhost, it set dev.undpgeohub.org for testing - const origin = url.origin.indexOf('localhost') > -1 ? env.GEOHUB_API_ENDPOINT : url.origin; + // set URL origin if URL starts with /api + // if origin is localhost, it set dev.undpgeohub.org for testing + const origin = url.origin.indexOf('localhost') > -1 ? env.GEOHUB_API_ENDPOINT : url.origin; + if (style.style && style.style.sources) { Object.keys(style.style.sources).forEach((key) => { - const source = style.style.sources[key]; - if ('url' in source && source.url.startsWith('/api')) { - source.url = `${origin}${source.url}`; - } else if ('tiles' in source) { - source.tiles.forEach((tile) => { - if (tile.startsWith('/api')) { - tile = `${origin}${tile}`; - } - }); + const source = style.style?.sources[key]; + if (source) { + if ('url' in source && source.url?.startsWith('/api')) { + source.url = `${origin}${source.url}`; + } else if ('tiles' in source) { + source.tiles?.forEach((tile) => { + if (tile.startsWith('/api')) { + tile = `${origin}${tile}`; + } + }); + } } }); + } - style.links = createStyleLinks(style, url); + style.links = createStyleLinks(style, url); - if (style.style) { - Object.keys(style.style.sources).forEach((srcId) => { - const source = style.style.sources[srcId]; - if (source.type !== 'raster') return; - if (['bing', 'terrarium'].includes(srcId)) return; // don't replace to titiler url for these sources - // if titiler URL saved in database is different from actual server settings, replace URL origin to env varaible one. - const rasterSource = source as RasterSourceSpecification; - const tiles = rasterSource.tiles; - const titilerUrl = new URL(env.TITILER_ENDPOINT); - for (let i = 0; i < tiles.length; i++) { - const url = new URL(tiles[i]); - if (url.origin !== titilerUrl.origin) { - tiles[i] = tiles[i].replace(url.origin, titilerUrl.origin); - } + if (style.style && style.style.sources) { + Object.keys(style.style.sources).forEach((srcId) => { + const source = style.style.sources[srcId]; + if (source.type !== 'raster') return; + if (['bing', 'terrarium'].includes(srcId)) return; // don't replace to titiler url for these sources + // if titiler URL saved in database is different from actual server settings, replace URL origin to env varaible one. + const rasterSource = source as RasterSourceSpecification; + const tiles = rasterSource.tiles; + const titilerUrl = new URL(env.TITILER_ENDPOINT); + for (let i = 0; i < tiles.length; i++) { + const url = new URL(tiles[i]); + if (url.origin !== titilerUrl.origin) { + tiles[i] = tiles[i].replace(url.origin, titilerUrl.origin); } - }); - - // delete admin layer - const adminLayerName = 'cgaz'; - if (style.style.sources[adminLayerName]) { - style.style.layers = style.style.layers.filter( - (l) => 'source' in l && l.source !== adminLayerName - ); - delete style.style.sources[adminLayerName]; } + }); - // there might be some updated on base style between saved style and original one. - // Here, it updates base style from the latest. + // delete admin layer + const adminLayerName = 'cgaz'; + if (style.style.sources[adminLayerName]) { + style.style.layers = style.style.layers.filter( + (l) => 'source' in l && l.source !== adminLayerName + ); + delete style.style.sources[adminLayerName]; + } - // check which base style is used - let baseStyle: StyleSpecification = voyagerStyle as unknown as StyleSpecification; - if (style.style.sources['bing']) { - // aerial - baseStyle = aerialStyle as unknown as StyleSpecification; - } else { - // check color of background layer to identify base style - const backgroudLayer: BackgroundLayerSpecification = style.style.layers.find( - (l) => l.type === 'background' - ) as BackgroundLayerSpecification; - if (backgroudLayer) { - if (backgroudLayer.paint['background-color'] === '#0e0e0e') { - // dark style: https://github.com/UNDP-Data/style/blob/main/assets/dark/background.yml - baseStyle = darkStyle as unknown as StyleSpecification; - } else if (backgroudLayer.paint['background-color'] === '#fafaf8') { - // positron style: https://github.com/UNDP-Data/style/blob/main/assets/positron/background.yml - baseStyle = positronStyle as unknown as StyleSpecification; - } + // there might be some updated on base style between saved style and original one. + // Here, it updates base style from the latest. + + // check which base style is used + let baseStyle: StyleSpecification = voyagerStyle as unknown as StyleSpecification; + if (style.style.sources['bing']) { + // aerial + baseStyle = aerialStyle as unknown as StyleSpecification; + } else { + // check color of background layer to identify base style + const backgroudLayer: BackgroundLayerSpecification = style.style.layers.find( + (l) => l.type === 'background' + ) as BackgroundLayerSpecification; + if (backgroudLayer) { + if (backgroudLayer.paint['background-color'] === '#0e0e0e') { + // dark style: https://github.com/UNDP-Data/style/blob/main/assets/dark/background.yml + baseStyle = darkStyle as unknown as StyleSpecification; + } else if (backgroudLayer.paint['background-color'] === '#fafaf8') { + // positron style: https://github.com/UNDP-Data/style/blob/main/assets/positron/background.yml + baseStyle = positronStyle as unknown as StyleSpecification; } } + } - // update sprite and glyphs - style.style.sprite = resolveSpriteUrl(baseStyle.sprite, url.origin); - style.style.glyphs = baseStyle.glyphs; + // update sprite and glyphs + style.style.sprite = resolveSpriteUrl(baseStyle.sprite, url.origin); + style.style.glyphs = baseStyle.glyphs; - // add source from the latest style if does not exist - Object.keys(baseStyle.sources).forEach((srcName) => { - if (style.style.sources[srcName]) return; - const newSource = baseStyle.sources[srcName]; - style.style.sources[srcName] = newSource; - }); + // add source from the latest style if does not exist + Object.keys(baseStyle.sources).forEach((srcName) => { + if (style.style.sources[srcName]) return; + const newSource = baseStyle.sources[srcName]; + style.style.sources[srcName] = newSource; + }); - // update base layer style - const updatedLayers: LayerSpecification[] = JSON.parse(JSON.stringify(baseStyle.layers)); - // get the total layer length exclude geohub layer for saved style - const totalBaseLayerLength = style.style.layers.filter( - (l) => style.layers.map((_l) => _l.id).includes(l.id) === false - ).length; - for (const savedLayer of style.style.layers) { - // // skip if not geohub layer - if (baseStyle.layers.find((l) => l.id === savedLayer.id)) continue; - const currentIndex = style.style.layers.indexOf(savedLayer); + // update base layer style + const updatedLayers: LayerSpecification[] = JSON.parse(JSON.stringify(baseStyle.layers)); + // get the total layer length exclude geohub layer for saved style + const totalBaseLayerLength = style.style.layers.filter( + (l) => style.layers.map((_l) => _l.id).includes(l.id) === false + ).length; + for (const savedLayer of style.style.layers) { + // // skip if not geohub layer + if (baseStyle.layers.find((l) => l.id === savedLayer.id)) continue; + const currentIndex = style.style.layers.indexOf(savedLayer); - if (currentIndex > totalBaseLayerLength) { - // if it exists in the last part of layers - updatedLayers.push(savedLayer); + if (currentIndex > totalBaseLayerLength) { + // if it exists in the last part of layers + updatedLayers.push(savedLayer); + } else { + // if it exists in the middle of layers (for raster mostly) + const beforeOld = style.style.layers[currentIndex - 1]; + const beforeNew = updatedLayers[currentIndex - 1]; + if (beforeOld.id === beforeNew.id) { + // if layer IDs before this layer are the same, insert it at the same index + updatedLayers.splice(currentIndex, 0, savedLayer); } else { - // if it exists in the middle of layers (for raster mostly) - const beforeOld = style.style.layers[currentIndex - 1]; - const beforeNew = updatedLayers[currentIndex - 1]; - if (beforeOld.id === beforeNew.id) { - // if layer IDs before this layer are the same, insert it at the same index - updatedLayers.splice(currentIndex, 0, savedLayer); - } else { - // otherwise insert layer before first symbol layer (style structure might have been changed at all) - const firstSymbolLayerId = getFirstSymbolLayerId(updatedLayers); - let idx = updatedLayers.length - 1; - if (firstSymbolLayerId) { - idx = updatedLayers.findIndex((l) => l.id === firstSymbolLayerId); - } - updatedLayers.splice(idx, 0, savedLayer); + // otherwise insert layer before first symbol layer (style structure might have been changed at all) + const firstSymbolLayerId = getFirstSymbolLayerId(updatedLayers); + let idx = updatedLayers.length - 1; + if (firstSymbolLayerId) { + idx = updatedLayers.findIndex((l) => l.id === firstSymbolLayerId); } + updatedLayers.splice(idx, 0, savedLayer); } } - style.style.layers = [...updatedLayers]; } + style.style.layers = [...updatedLayers]; + } - // if text-font is not set, use default font. - style.style.layers.forEach((l) => { - if (l.type === 'symbol') { - if (l.layout['text-field'] && !l.layout['text-font']) { - l.layout['text-font'] = [DefaultUserConfig.LabelTextFont]; - } + // if text-font is not set, use default font. + style.style?.layers?.forEach((l) => { + if (l.type === 'symbol') { + if (l.layout['text-field'] && !l.layout['text-font']) { + l.layout['text-font'] = [DefaultUserConfig.LabelTextFont]; } - }); + } + }); - if (style.layers) { - const currentTime = new Date(); - const delLayerIds: string[] = []; - const inaccesibleLayerIds: string[] = []; - for (const l of style.layers) { - const dataType = l.dataset.properties.tags?.find((t) => t.key === 'type')?.value; - if (dataType?.toLowerCase() === 'stac') { - const stac = l.dataset.properties.tags?.find((t) => t.key === 'stac')?.value; - if (stac === 'microsoft-pc') { - // check the token expiry datatime and update if it is expired - const collection = l.dataset.properties.tags?.find((t) => t.key === 'collection'); - const stacInfo = await getSTAC(stac); - const microsoft = new MicrosoftPlanetaryStac(collection.value, stacInfo); - const source = style.style.sources[l.id] as RasterSourceSpecification; - const data = await microsoft.updateSasToken( - l.dataset, - source, - currentTime, - updateMosaicJsonBlob - ); - l.dataset = data.dataset; - style.style.sources[l.id] = data.source; - } - } else { + if (style.layers) { + const currentTime = new Date(); + const delLayerIds: string[] = []; + const inaccesibleLayerIds: string[] = []; + for (const l of style.layers) { + const dataType = l.dataset?.properties.tags?.find((t) => t.key === 'type')?.value; + if (dataType?.toLowerCase() === 'stac') { + const stac = l.dataset.properties.tags?.find((t) => t.key === 'stac')?.value; + if (stac === 'microsoft-pc') { + // check the token expiry datatime and update if it is expired + const collection = l.dataset.properties.tags?.find((t) => t.key === 'collection'); + const stacInfo = await getSTAC(stac); + const microsoft = new MicrosoftPlanetaryStac(collection.value, stacInfo); + const source = style.style.sources[l.id] as RasterSourceSpecification; + const data = await microsoft.updateSasToken( + l.dataset, + source, + currentTime, + updateMosaicJsonBlob + ); + l.dataset = data.dataset; + style.style.sources[l.id] = data.source; + } + } else { + if (l.dataset) { // regenerate geohub dataset object - l.dataset = await getDatasetById(client, l.dataset.properties.id, is_superuser, email); + l.dataset = await getDatasetById(l.dataset.properties.id, is_superuser, email); if (l.dataset) { l.dataset.properties = await createDatasetLinks( l.dataset, @@ -268,9 +267,7 @@ export const getStyleById = async (id: number, url: URL, email?: string, is_supe for (const tile of source.tiles) { const href = new URL(tile); href.searchParams.set('url', tileUrl); - const newTile = `${href.origin}${decodeURIComponent(href.pathname)}${ - href.search - }`; + const newTile = `${href.origin}${decodeURIComponent(href.pathname)}${href.search}`; newTiles.push(newTile); } source.tiles = newTiles; @@ -304,43 +301,39 @@ export const getStyleById = async (id: number, url: URL, email?: string, is_supe } } } + } - // delete all layers and sources if some of them are already unregistered from the database. - delLayerIds.forEach((id) => { - style.layers = [...style.layers.filter((l) => l.id !== id)]; + // delete all layers and sources if some of them are already unregistered from the database. + delLayerIds.forEach((id) => { + style.layers = [...style.layers.filter((l) => l.id !== id)]; - const mapLayer = style.style.layers.find((l) => l.id === id) as - | RasterLayerSpecification - | VectorLayerSpecification - | HillshadeLayerSpecification; - if (mapLayer) { - const sourceId = mapLayer.source; - style.style.layers = [...style.style.layers.filter((l) => l.id !== id)]; - if (style.style.sources[sourceId]) { - delete style.style.sources[sourceId]; - } + const mapLayer = style.style.layers.find((l) => l.id === id) as + | RasterLayerSpecification + | VectorLayerSpecification + | HillshadeLayerSpecification; + if (mapLayer) { + const sourceId = mapLayer.source; + style.style.layers = [...style.style.layers.filter((l) => l.id !== id)]; + if (style.style.sources[sourceId]) { + delete style.style.sources[sourceId]; } - }); - // delete layers only from style.json if user does not have permission to access - inaccesibleLayerIds.forEach((id) => { - const mapLayer = style.style.layers.find((l) => l.id === id) as - | RasterLayerSpecification - | VectorLayerSpecification - | HillshadeLayerSpecification; - if (mapLayer) { - const sourceId = mapLayer.source; - style.style.layers = [...style.style.layers.filter((l) => l.id !== id)]; - if (style.style.sources[sourceId]) { - delete style.style.sources[sourceId]; - } + } + }); + // delete layers only from style.json if user does not have permission to access + inaccesibleLayerIds.forEach((id) => { + const mapLayer = style.style.layers.find((l) => l.id === id) as + | RasterLayerSpecification + | VectorLayerSpecification + | HillshadeLayerSpecification; + if (mapLayer) { + const sourceId = mapLayer.source; + style.style.layers = [...style.style.layers.filter((l) => l.id !== id)]; + if (style.style.sources[sourceId]) { + delete style.style.sources[sourceId]; } - }); - } - - return style; - } catch (err) { - console.error(err); - } finally { - dbm.end(); + } + }); } + + return style; }; diff --git a/sites/geohub/src/lib/server/helpers/getStyleCount.ts b/sites/geohub/src/lib/server/helpers/getStyleCount.ts index 04adf2be3..93d6ff216 100644 --- a/sites/geohub/src/lib/server/helpers/getStyleCount.ts +++ b/sites/geohub/src/lib/server/helpers/getStyleCount.ts @@ -1,22 +1,21 @@ -import DatabaseManager from '$lib/server/DatabaseManager'; +import { sql, type SQL } from 'drizzle-orm'; +import { db } from '$lib/server/db'; /** * Get the total count of styles stored in database * GET: ./api/style/count */ -export const getStyleCount = async (where: string, values: string[]) => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: `SELECT count(*) as count FROM geohub.style x ${where}`, - values: values - }; - - const res = await client.query(query); - - return Number(res.rows[0].count); - } finally { - dbm.end(); +export const getStyleCount = async (where: SQL) => { + const sqlChunks: SQL[] = []; + sqlChunks.push(sql.raw(`SELECT count(*) as count FROM geohub.style x`)); + if (where) { + sqlChunks.push(where); + } + const finalSql: SQL = sql.join(sqlChunks, sql.raw(' ')); + const data = await db.execute(finalSql); + if (data.length === 0) { + return 0; + } else { + return data[0].count as number; } }; diff --git a/sites/geohub/src/lib/server/helpers/getStyleStarCount.ts b/sites/geohub/src/lib/server/helpers/getStyleStarCount.ts index 421520aa5..b4d093486 100644 --- a/sites/geohub/src/lib/server/helpers/getStyleStarCount.ts +++ b/sites/geohub/src/lib/server/helpers/getStyleStarCount.ts @@ -1,21 +1,13 @@ -import type { PoolClient } from 'pg'; +import { db } from '$lib/server/db'; +import { styleFavouriteInGeohub } from '../schema'; +import { count, eq } from 'drizzle-orm'; -export const getStyleStarCount = async (client: PoolClient, style_id: number) => { - const query = { - text: ` - SELECT count(*) as stars - FROM geohub.style_favourite - WHERE style_id = $1 - GROUP BY style_id - `, - values: [style_id] - }; +export const getStyleStarCount = async (style_id: number) => { + const result = await db + .select({ count: count() }) + .from(styleFavouriteInGeohub) + .where(eq(styleFavouriteInGeohub.styleId, style_id)) + .groupBy(styleFavouriteInGeohub.styleId); - const res = await client.query(query); - - if (res.rowCount === 0) { - return 0; - } else { - return res.rows[0]['stars']; - } + return result.length === 0 ? 0 : result[0].count; }; diff --git a/sites/geohub/src/lib/server/helpers/index.ts b/sites/geohub/src/lib/server/helpers/index.ts index 366f6ed11..656f28290 100644 --- a/sites/geohub/src/lib/server/helpers/index.ts +++ b/sites/geohub/src/lib/server/helpers/index.ts @@ -14,10 +14,8 @@ export * from './getStyleStarCount'; export * from './getVectorMetadata'; export * from './pageNumber'; export * from './getStyleById'; -export * from './getMapStats'; export * from './getDatasetStarCount'; export * from './generateAzureBlobSasToken'; -export * from './upsertDataset'; export * from './getDatasetById'; export * from './isSuperuser'; export * from './upload'; @@ -29,7 +27,6 @@ export * from './updateMosaicJsonBlob'; export * from './getDefaultLayerStyle'; export * from './getDatasetStats'; export * from './stac'; -export * from './searchUsersByEmail'; export * from './resolveSpriteUrl'; export * from './isGeoHubBlobStorage'; export * from './recolorPngDataUrl'; diff --git a/sites/geohub/src/lib/server/helpers/isSuperuser.ts b/sites/geohub/src/lib/server/helpers/isSuperuser.ts index 95e37cd16..42738deae 100644 --- a/sites/geohub/src/lib/server/helpers/isSuperuser.ts +++ b/sites/geohub/src/lib/server/helpers/isSuperuser.ts @@ -1,16 +1,12 @@ -import DatabaseManager from '$lib/server/DatabaseManager'; +import { db } from '$lib/server/db'; +import { eq } from 'drizzle-orm'; +import { superuserInGeohub } from '$lib/server/schema'; export const isSuperuser = async (user_email: string) => { - const dbm = new DatabaseManager(); - try { - const client = await dbm.start(); - const query = { - text: `SELECT user_email FROM geohub.superuser WHERE user_email = $1`, - values: [user_email] - }; - const res = await client.query(query); - return res.rowCount > 0; - } finally { - await dbm.end(); - } + const users = await db + .select({ user_email: superuserInGeohub.userEmail }) + .from(superuserInGeohub) + .where(eq(superuserInGeohub.userEmail, user_email)); + + return users.length > 0 ? true : false; }; diff --git a/sites/geohub/src/lib/server/helpers/loadStorymapById.ts b/sites/geohub/src/lib/server/helpers/loadStorymapById.ts index 4a28e01b6..b802825e8 100644 --- a/sites/geohub/src/lib/server/helpers/loadStorymapById.ts +++ b/sites/geohub/src/lib/server/helpers/loadStorymapById.ts @@ -2,9 +2,8 @@ import { AccessLevel, Permission } from '$lib/config/AppConfig'; import { getDomainFromEmail } from '$lib/helper'; import type { StoryMapConfig } from '$lib/types'; import { error } from '@sveltejs/kit'; -import DatabaseManager from '../DatabaseManager'; -import StorymapManager from '../StorymapManager'; -import { isSuperuser } from './isSuperuser'; +import StorymapManager from '$lib/server/StorymapManager'; +import { isSuperuser } from '$lib/server/helpers/isSuperuser'; export const loadStorymapById = async ( id: string, @@ -18,36 +17,32 @@ export const loadStorymapById = async ( is_superuser = await isSuperuser(user_email); } - let storymap: StoryMapConfig | undefined; + const sm = new StorymapManager(); + const storymap: StoryMapConfig = (await sm.getById( + id, + is_superuser, + user_email + )) as unknown as StoryMapConfig; + if (!storymap) { + error(404, { message: `No storymap found.` }); + } - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const sm = new StorymapManager(); - storymap = await sm.getById(client, id, is_superuser, user_email); - if (!storymap) { - error(404, { message: `No storymap found.` }); - } + if (storymap.style) { + storymap.style = `${url.origin}${storymap.style}`; + } + storymap.chapters.forEach((ch) => { + ch.style = `${url.origin}${ch.style}`; + }); - if (storymap.style) { - storymap.style = `${url.origin}${storymap.style}`; + storymap.links = storymap.links?.map((l) => { + const _url = new URL(decodeURI(l.href), url.origin); + const subUrl = _url.searchParams.get('url'); + if (subUrl) { + _url.searchParams.set('url', new URL(subUrl, url.origin).href); } - storymap.chapters.forEach((ch) => { - ch.style = `${url.origin}${ch.style}`; - }); - - storymap.links = storymap.links?.map((l) => { - const _url = new URL(decodeURI(l.href), url.origin); - const subUrl = _url.searchParams.get('url'); - if (subUrl) { - _url.searchParams.set('url', new URL(subUrl, url.origin).href); - } - l.href = decodeURI(_url.href); - return l; - }); - } finally { - dbm.end(); - } + l.href = decodeURI(_url.href); + return l; + }); if (!embed) { const accessLevel = storymap.access_level; diff --git a/sites/geohub/src/lib/server/helpers/searchUsersByEmail.ts b/sites/geohub/src/lib/server/helpers/searchUsersByEmail.ts deleted file mode 100644 index 11b2577d0..000000000 --- a/sites/geohub/src/lib/server/helpers/searchUsersByEmail.ts +++ /dev/null @@ -1,24 +0,0 @@ -import DatabaseManager from '$lib/server/DatabaseManager'; - -export const searchUsersByEmail = async (query: string, limit: number) => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const sql = { - text: ` - SELECT id, user_email - FROM geohub.users - WHERE user_email ILIKE $1 - ORDER BY user_email - LIMIT $2 - `, - values: [`%${query}%`, limit] - }; - - const res = await client.query(sql); - - return res.rows as { id: string; user_email: string }[]; - } finally { - dbm.end(); - } -}; diff --git a/sites/geohub/src/lib/server/helpers/stac.ts b/sites/geohub/src/lib/server/helpers/stac.ts index c6d51f09e..a6aab975b 100644 --- a/sites/geohub/src/lib/server/helpers/stac.ts +++ b/sites/geohub/src/lib/server/helpers/stac.ts @@ -1,217 +1,66 @@ -import DatabaseManager from '$lib/server/DatabaseManager'; import type { Stac } from '$lib/types'; import { error } from '@sveltejs/kit'; -import type { PoolClient } from 'pg'; - -export const getSTACs = async (type?: string) => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - if (type && !['api', 'catalog'].includes(type)) { - error(400, { message: `invalid type. type param should be either api or catalog.` }); - } - const query = { - text: `SELECT - id, - name, - url, - type, - providers, - createdat, - created_user, - updatedat, - updated_user - FROM geohub.stac - ${type ? 'WHERE type=$1' : ''} - `, - values: [] +import { db } from '$lib/server/db'; +import { stacInGeohub } from '$lib/server/schema'; +import { eq } from 'drizzle-orm'; + +export const getSTACs = async (id?: string) => { + let options; + if (id) { + options = { + where: eq(stacInGeohub.id, id) }; - - if (type) { - query.values.push(type); - } - - const res = await client.query(query); - const stacs: Stac[] = res.rows; - return stacs; - } finally { - dbm.end(); } + + const stacs: Stac[] = (await db.query.stacInGeohub.findMany(options)) as Stac[]; + return stacs; }; export const getSTAC = async (id: string) => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const stac = await getSTACById(client, id); - return stac; - } finally { - dbm.end(); - } + const stacs = await getSTACs(id); + return stacs && stacs.length > 0 ? stacs[0] : undefined; }; export const upsertSTAC = async (stac: Stac, user_email: string) => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const requiredProps = ['id', 'name', 'url', 'type']; - requiredProps.forEach((prop) => { - if (prop in stac) return; - error(400, `${prop} property is required`); - }); - - const now = new Date().toISOString(); - - const query = { - text: `INSERT INTO geohub.stac - ( - id, - name, - url, - type, - providers, - createdat, - created_user - ) - values ( - $1, - $2, - $3, - $4, - $5, - $6::timestamptz, - $7 - ) - ON CONFLICT (id) - DO - UPDATE - SET - name=$2, - url=$3, - type=$4, - providers=$5, - updatedat=$6, - updated_user=$7 - `, - values: [ - stac.id, - stac.name, - stac.url, - stac.type, - JSON.stringify(stac.providers), - now, - user_email - ] - }; - - await client.query(query); - - return stac; - } finally { - dbm.end(); - } + const requiredProps = ['id', 'name', 'url', 'type']; + requiredProps.forEach((prop) => { + if (prop in stac) return; + error(400, `${prop} property is required`); + }); + + const now = new Date().toISOString(); + + const data = await db + .insert(stacInGeohub) + .values({ + id: stac.id, + name: stac.name, + url: stac.url, + type: stac.type, + providers: stac.providers, + createdat: now, + createdUser: user_email + }) + .onConflictDoUpdate({ + target: stacInGeohub.id, + set: { + name: stac.name, + url: stac.url, + type: stac.type, + providers: stac.providers, + updatedat: now, + updatedUser: user_email + } + }) + .returning(); + + return data; }; export const deleteSTAC = async (id: string) => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const stac = await getSTACById(client, id); - if (!stac) { - error(404, { message: 'Not found' }); - } - - const query = { - text: `DELETE FROM geohub.stac WHERE id=$1`, - values: [id] - }; - - await client.query(query); - - return; - } finally { - dbm.end(); - } -}; - -const getSTACById = async (client: PoolClient, id: string) => { - const query = { - text: `SELECT - id, - name, - url, - type, - providers, - createdat, - created_user, - updatedat, - updated_user - FROM geohub.stac - WHERE id=$1 - `, - values: [id] - }; - - const res = await client.query(query); - const stac: Stac = res.rows.length > 0 ? res.rows[0] : undefined; - return stac; -}; - -export const getProductDetails = async ( - stac_id: string, - collection_id: string, - product_id: string -) => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); - - const query = { - text: `SELECT a.stac_id, - a.collection_id, - a.product_id , - a.assets, - b.label, - b.expression, - b.description - FROM geohub.stac_collection_product - AS a INNER JOIN geohub.product AS b - ON a.product_id = b.id - WHERE stac_id=$1 AND collection_id=$2 AND product_id=$3`, - values: [stac_id, collection_id, product_id] - }; - try { - const res = await client.query(query); - return res.rows.length > 0 ? res.rows[0] : {}; - } catch (err) { - console.log(err); - await dbm.transactionRollback(); - error(500, err); - } finally { - await dbm.end(); - } -}; - -export const deleteProduct = async (stac_id: string, collection_id: string, product_id: string) => { - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - - const query = { - text: `DELETE FROM geohub.product WHERE stac_id=$1 AND collection_id=$2 AND product_id=$3`, - values: [stac_id, collection_id, product_id] - }; - - try { - const res = await client.query(query); - if (res.rowCount === 0) { - error(404, { message: `${product_id} does not exist in the database` }); - } - return { - message: 'Product deleted' - }; - } catch (err) { - await dbm.transactionRollback(); - error(500, err); - } finally { - await dbm.transactionEnd(); + const stac = await getSTAC(id); + if (!stac) { + error(404, { message: 'Not found' }); } + await db.delete(stacInGeohub).where(eq(stacInGeohub.id, id)); }; diff --git a/sites/geohub/src/lib/server/helpers/upsertDataset.ts b/sites/geohub/src/lib/server/helpers/upsertDataset.ts deleted file mode 100644 index 9bf78364a..000000000 --- a/sites/geohub/src/lib/server/helpers/upsertDataset.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { DatasetFeature } from '$lib/types'; -import DatabaseManager from '$lib/server/DatabaseManager'; -import TagManager from '$lib/server/TagManager'; -import DatasetManager from '$lib/server/DatasetManager'; - -export const upsertDataset = async (feature: DatasetFeature) => { - const dbm = new DatabaseManager(); - try { - console.debug(`dataset (id=${feature.properties.id}) started registering`); - const client = await dbm.transactionStart(); - - const dsManager = new DatasetManager(feature); - - const tags: TagManager = new TagManager(); - - dsManager.addTags(tags); - - await tags.insert(client); - console.debug(`${tags.getTags().length} tags were registered into PostGIS.`); - - dsManager.updateTags(tags); - - await dsManager.upsert(client); - console.debug(`dataset (id=${feature.properties.id}) was registered into PostGIS.`); - - await tags.cleanup(client); - console.debug(`unused tags were cleaned`); - - console.debug(`dataset (id=${feature.properties.id}) ended registering`); - } catch (e) { - console.error(e); - await dbm.transactionRollback(); - throw e; - } finally { - await dbm.transactionEnd(); - } -}; diff --git a/sites/geohub/src/lib/server/helpers/upsertUser.ts b/sites/geohub/src/lib/server/helpers/upsertUser.ts index b6fd825e2..f946a9f12 100644 --- a/sites/geohub/src/lib/server/helpers/upsertUser.ts +++ b/sites/geohub/src/lib/server/helpers/upsertUser.ts @@ -1,22 +1,18 @@ -import DatabaseManager from '$lib/server/DatabaseManager'; -import { error } from '@sveltejs/kit'; +import { generateHashKey } from '$lib/helper'; +import { db } from '$lib/server/db'; +import { usersInGeohub } from '$lib/server/schema'; export const upsertUser = async (user_email: string) => { - const dbm = new DatabaseManager(); - try { - const client = await dbm.start(); - - const query = { - text: ` - INSERT INTO geohub.users (id, user_email) values(MD5($1), $1) - ON CONFLICT (id) DO UPDATE SET lastaccessedat = now() - `, - values: [user_email] - }; - await client.query(query); - } catch (e) { - error(500, e); - } finally { - await dbm.end(); - } + await db + .insert(usersInGeohub) + .values({ + id: generateHashKey(user_email), + userEmail: user_email + }) + .onConflictDoUpdate({ + target: [usersInGeohub.id], + set: { + lastaccessedat: new Date().toISOString() + } + }); }; diff --git a/sites/geohub/src/lib/server/schema.ts b/sites/geohub/src/lib/server/schema.ts new file mode 100644 index 000000000..46b1f6fdd --- /dev/null +++ b/sites/geohub/src/lib/server/schema.ts @@ -0,0 +1,441 @@ +import { + // pgTable, + pgSchema, + varchar, + integer, + unique, + json, + index, + serial, + timestamp, + boolean, + geometry, + uuid, + doublePrecision, + jsonb, + foreignKey, + primaryKey, + smallint +} from 'drizzle-orm/pg-core'; +// import { sql } from 'drizzle-orm'; + +export const geohub = pgSchema('geohub'); + +export const countryInGeohub = geohub.table('country', { + iso3: varchar('iso_3').primaryKey().notNull(), + isoCode: integer('iso_code').notNull(), + iso2: varchar('iso_2'), + name: varchar('name').notNull(), + region1Code: integer('region1_code').notNull(), + region1Name: varchar('region1_name').notNull(), + region2Code: integer('region2_code').notNull(), + region2Name: varchar('region2_name').notNull(), + region3Code: integer('region3_code').notNull(), + region3Name: varchar('region3_name').notNull() +}); + +export const userSettingsInGeohub = geohub.table( + 'user_settings', + { + userEmail: varchar('user_email', { length: 100 }).notNull(), + settings: json('settings') + }, + (table) => { + return { + constraintName: unique('constraint_name').on(table.userEmail) + }; + } +); + +export const tagInGeohub = geohub.table( + 'tag', + { + id: serial('id').primaryKey().notNull(), + value: varchar('value').notNull(), + key: varchar('key').notNull() + }, + (table) => { + return { + idxKeyValue: index('tag_idx_key_value').using( + 'btree', + table.key.asc().nullsLast(), + table.value.asc().nullsLast() + ), + idxValue: index('tag_idx_value').using('btree', table.value.asc().nullsLast()) + }; + } +); + +export const styleInGeohub = geohub.table('style', { + id: serial('id').primaryKey().notNull(), + name: varchar('name').notNull(), + style: json('style').notNull(), + createdat: timestamp('createdat', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + updatedat: timestamp('updatedat', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + layers: json('layers'), + accessLevel: integer('access_level').default(1).notNull(), + createdUser: varchar('created_user', { length: 100 }), + updatedUser: varchar('updated_user', { length: 100 }) +}); + +export const usersInGeohub = geohub.table('users', { + id: varchar('id').primaryKey().notNull(), + userEmail: varchar('user_email', { length: 100 }).notNull(), + signupat: timestamp('signupat', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + lastaccessedat: timestamp('lastaccessedat', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull() +}); + +export const datasetInGeohub = geohub.table( + 'dataset', + { + id: varchar('id').primaryKey().notNull(), + url: varchar('url').notNull(), + isRaster: boolean('is_raster').notNull(), + license: varchar('license'), + bounds: geometry('bounds', { type: 'polygon', srid: 4326 }).notNull(), + createdat: timestamp('createdat', { withTimezone: true, mode: 'string' }).notNull(), + updatedat: timestamp('updatedat', { withTimezone: true, mode: 'string' }), + name: varchar('name'), + description: varchar('description'), + createdUser: varchar('created_user', { length: 100 }).notNull(), + updatedUser: varchar('updated_user', { length: 100 }), + accessLevel: integer('access_level').default(3).notNull() + }, + (table) => { + return { + boundsGeomIdx: index('dataset_bounds_geom_idx').using('gist', table.bounds.asc().nullsLast()) + }; + } +); + +export const superuserInGeohub = geohub.table('superuser', { + userEmail: varchar('user_email', { length: 100 }).primaryKey().notNull(), + createdat: timestamp('createdat', { withTimezone: true, mode: 'string' }).defaultNow().notNull() +}); + +export const stacInGeohub = geohub.table('stac', { + id: varchar('id').primaryKey().notNull(), + name: varchar('name').notNull(), + url: varchar('url').notNull(), + type: varchar('type').notNull(), + providers: json('providers'), + createdat: timestamp('createdat', { withTimezone: true, mode: 'string' }).defaultNow().notNull(), + createdUser: varchar('created_user', { length: 100 }).notNull(), + updatedat: timestamp('updatedat', { withTimezone: true, mode: 'string' }), + updatedUser: varchar('updated_user', { length: 100 }) +}); + +export const productInGeohub = geohub.table('product', { + id: varchar('id').primaryKey().notNull(), + label: varchar('label').notNull(), + expression: varchar('expression').notNull(), + description: varchar('description').notNull() +}); + +export const storymapInGeohub = geohub.table('storymap', { + id: uuid('id').primaryKey().notNull(), + title: varchar('title').notNull(), + logo: varchar('logo'), + subtitle: varchar('subtitle'), + byline: varchar('byline'), + footer: varchar('footer'), + templateId: varchar('template_id').notNull(), + styleId: integer('style_id'), + baseStyleId: varchar('base_style_id'), + accessLevel: integer('access_level').default(1).notNull(), + createdat: timestamp('createdat', { withTimezone: true, mode: 'string' }).notNull(), + createdUser: varchar('created_user').notNull(), + updatedat: timestamp('updatedat', { withTimezone: true, mode: 'string' }), + updatedUser: varchar('updated_user'), + showProgress: boolean('show_progress').default(true).notNull() +}); + +export const storymapChapterInGeohub = geohub.table('storymap_chapter', { + id: uuid('id').primaryKey().notNull(), + title: varchar('title').notNull(), + description: varchar('description').notNull(), + image: varchar('image'), + alignment: varchar('alignment').notNull(), + mapInteractive: boolean('map_interactive').default(false).notNull(), + mapNavigationPosition: varchar('map_navigation_position').notNull(), + mapAnimation: varchar('map_animation').notNull(), + rotateAnimation: boolean('rotate_animation').default(false).notNull(), + spinglobe: boolean('spinglobe').default(false).notNull(), + hidden: boolean('hidden').default(false).notNull(), + center: geometry('center', { type: 'point', srid: 4326 }).notNull(), + zoom: doublePrecision('zoom').notNull(), + bearing: doublePrecision('bearing').default(0).notNull(), + pitch: doublePrecision('pitch').default(0).notNull(), + styleId: integer('style_id'), + baseStyleId: varchar('base_style_id'), + onChapterEnter: jsonb('on_chapter_enter'), + onChapterExit: jsonb('on_chapter_exit'), + createdat: timestamp('createdat', { withTimezone: true, mode: 'string' }).notNull(), + createdUser: varchar('created_user').notNull(), + updatedat: timestamp('updatedat', { withTimezone: true, mode: 'string' }), + updatedUser: varchar('updated_user'), + cardHidden: boolean('card_hidden').default(false).notNull(), + legendPosition: varchar('legend_position').default('bottom-left'), + showLegend: boolean('show_legend').default(true).notNull() +}); + +export const licenseInGeohub = geohub.table('license', { + id: serial('id').primaryKey().notNull(), + name: varchar('name').notNull() +}); + +export const datasetTagInGeohub = geohub.table( + 'dataset_tag', + { + datasetId: varchar('dataset_id').notNull(), + tagId: serial('tag_id').notNull() + }, + (table) => { + return { + fkDatasetToDatasetTag: foreignKey({ + columns: [table.datasetId], + foreignColumns: [datasetInGeohub.id], + name: 'fk_dataset_to_dataset_tag' + }).onDelete('cascade'), + fkTagToDatasetTag: foreignKey({ + columns: [table.tagId], + foreignColumns: [tagInGeohub.id], + name: 'fk_tag_to_dataset_tag' + }).onDelete('cascade'), + datasetTagPkey: primaryKey({ + columns: [table.datasetId, table.tagId], + name: 'dataset_tag_pkey' + }) + }; + } +); + +export const styleFavouriteInGeohub = geohub.table( + 'style_favourite', + { + styleId: integer('style_id').notNull(), + userEmail: varchar('user_email', { length: 100 }).notNull(), + savedat: timestamp('savedat', { withTimezone: true, mode: 'string' }).defaultNow().notNull() + }, + (table) => { + return { + fkStyleToStyleFavourite: foreignKey({ + columns: [table.styleId], + foreignColumns: [styleInGeohub.id], + name: 'FK_style_TO_style_favourite' + }).onDelete('cascade'), + styleFavouritePkey: primaryKey({ + columns: [table.styleId, table.userEmail], + name: 'style_favourite_pkey' + }) + }; + } +); + +export const datasetFavouriteInGeohub = geohub.table( + 'dataset_favourite', + { + datasetId: varchar('dataset_id').notNull(), + userEmail: varchar('user_email', { length: 100 }).notNull(), + savedat: timestamp('savedat', { withTimezone: true, mode: 'string' }).notNull() + }, + (table) => { + return { + fkDatasetToDatasetFavourite: foreignKey({ + columns: [table.datasetId], + foreignColumns: [datasetInGeohub.id], + name: 'FK_dataset_TO_dataset_favourite' + }).onDelete('cascade'), + datasetFavouritePkey: primaryKey({ + columns: [table.datasetId, table.userEmail], + name: 'dataset_favourite_pkey' + }) + }; + } +); + +export const storymapChaptersInGeohub = geohub.table( + 'storymap_chapters', + { + storymapId: uuid('storymap_id').notNull(), + chapterId: uuid('chapter_id').notNull(), + sequence: integer('sequence').notNull() + }, + (table) => { + return { + fkStorymapToStorymapChapters: foreignKey({ + columns: [table.storymapId], + foreignColumns: [storymapInGeohub.id], + name: 'fk_storymap_to_storymap_chapters' + }).onDelete('cascade'), + fkStorymapToStorymapChapter: foreignKey({ + columns: [table.chapterId], + foreignColumns: [storymapChapterInGeohub.id], + name: 'fk_storymap_to_storymap_chapter' + }).onDelete('cascade'), + storymapChaptersPkey: primaryKey({ + columns: [table.storymapId, table.chapterId], + name: 'storymap_chapters_pkey' + }) + }; + } +); + +export const storymapFavouriteInGeohub = geohub.table( + 'storymap_favourite', + { + storymapId: uuid('storymap_id').notNull(), + userEmail: varchar('user_email', { length: 100 }).notNull(), + savedat: timestamp('savedat', { withTimezone: true, mode: 'string' }).notNull() + }, + (table) => { + return { + fkStorymapToStorymapFavourite: foreignKey({ + columns: [table.storymapId], + foreignColumns: [storymapInGeohub.id], + name: 'FK_storymap_TO_storymap_favourite' + }).onDelete('cascade'), + storymapFavouritePkey: primaryKey({ + columns: [table.storymapId, table.userEmail], + name: 'storymap_favourite_pkey' + }) + }; + } +); + +export const datasetPermissionInGeohub = geohub.table( + 'dataset_permission', + { + datasetId: varchar('dataset_id').notNull(), + userEmail: varchar('user_email', { length: 100 }).notNull(), + permission: smallint('permission').default(1).notNull(), + createdat: timestamp('createdat', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedat: timestamp('updatedat', { withTimezone: true, mode: 'string' }) + }, + (table) => { + return { + fkDatasetToDatasetPermission: foreignKey({ + columns: [table.datasetId], + foreignColumns: [datasetInGeohub.id], + name: 'FK_dataset_TO_dataset_permission' + }).onDelete('cascade'), + datasetPermissionPkey: primaryKey({ + columns: [table.datasetId, table.userEmail], + name: 'dataset_permission_pkey' + }) + }; + } +); + +export const stacCollectionProductInGeohub = geohub.table( + 'stac_collection_product', + { + stacId: varchar('stac_id').notNull(), + collectionId: varchar('collection_id').notNull(), + productId: varchar('product_id').notNull(), + assets: varchar('assets').array().notNull(), + description: varchar('description') + }, + (table) => { + return { + stacCollectionProductProductIdFkey: foreignKey({ + columns: [table.productId], + foreignColumns: [productInGeohub.id], + name: 'stac_collection_product_product_id_fkey' + }), + stacCollectionProductPkey: primaryKey({ + columns: [table.stacId, table.collectionId, table.productId], + name: 'stac_collection_product_pkey' + }) + }; + } +); + +export const stylePermissionInGeohub = geohub.table( + 'style_permission', + { + styleId: integer('style_id').notNull(), + userEmail: varchar('user_email', { length: 100 }).notNull(), + permission: smallint('permission').default(1).notNull(), + createdat: timestamp('createdat', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedat: timestamp('updatedat', { withTimezone: true, mode: 'string' }) + }, + (table) => { + return { + fkStyleToStylePermission: foreignKey({ + columns: [table.styleId], + foreignColumns: [styleInGeohub.id], + name: 'FK_style_TO_style_permission' + }).onDelete('cascade'), + stylePermissionPkey: primaryKey({ + columns: [table.styleId, table.userEmail], + name: 'style_permission_pkey' + }) + }; + } +); + +export const storymapPermissionInGeohub = geohub.table( + 'storymap_permission', + { + storymapId: uuid('storymap_id').notNull(), + userEmail: varchar('user_email', { length: 100 }).notNull(), + permission: smallint('permission').default(1).notNull(), + createdat: timestamp('createdat', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedat: timestamp('updatedat', { withTimezone: true, mode: 'string' }) + }, + (table) => { + return { + fkStorymapToStorymapPermission: foreignKey({ + columns: [table.storymapId], + foreignColumns: [storymapInGeohub.id], + name: 'FK_storymap_TO_storymap_permission' + }).onDelete('cascade'), + storymapPermissionPkey: primaryKey({ + columns: [table.storymapId, table.userEmail], + name: 'storymap_permission_pkey' + }) + }; + } +); + +export const datasetDefaultstyleInGeohub = geohub.table( + 'dataset_defaultstyle', + { + datasetId: varchar('dataset_id').notNull(), + layerId: varchar('layer_id').notNull(), + layerType: varchar('layer_type').notNull(), + source: json('source').notNull(), + style: json('style').notNull(), + colormapName: varchar('colormap_name'), + classificationMethod: varchar('classification_method'), + createdUser: varchar('created_user', { length: 100 }).notNull(), + createdat: timestamp('createdat', { withTimezone: true, mode: 'string' }) + .defaultNow() + .notNull(), + updatedat: timestamp('updatedat', { withTimezone: true, mode: 'string' }), + updatedUser: varchar('updated_user', { length: 100 }), + classificationMethod2: varchar('classification_method_2') + }, + (table) => { + return { + 'fkDatasetToDatasetId,LayerType': foreignKey({ + columns: [table.datasetId], + foreignColumns: [datasetInGeohub.id], + name: 'FK_dataset_TO_dataset_id, layer_type' + }).onDelete('cascade'), + datasetDefaultstylePkey: primaryKey({ + columns: [table.datasetId, table.layerId, table.layerType], + name: 'dataset_defaultstyle_pkey' + }) + }; + } +); diff --git a/sites/geohub/src/routes/(app)/management/stac/api/[id]/+page.server.ts b/sites/geohub/src/routes/(app)/management/stac/api/[id]/+page.server.ts index 3f15e28f1..2d5663206 100644 --- a/sites/geohub/src/routes/(app)/management/stac/api/[id]/+page.server.ts +++ b/sites/geohub/src/routes/(app)/management/stac/api/[id]/+page.server.ts @@ -1,7 +1,7 @@ -import { upsertDataset } from '$lib/server/helpers'; import type { DatasetFeature } from '$lib/types'; import { fail, type Actions } from '@sveltejs/kit'; import type { PageServerLoad } from './$types'; +import DatasetManager from '$lib/server/DatasetManager'; export const load: PageServerLoad = async ({ parent }) => { const { stac } = await parent(); @@ -18,30 +18,27 @@ export const load: PageServerLoad = async ({ parent }) => { export const actions = { register: async (event) => { const { request, locals } = event; - try { - const session = await locals.auth(); - if (!session) { - return fail(403, { message: 'No permission' }); - } - const data = await request.formData(); + const session = await locals.auth(); + if (!session) { + return fail(403, { message: 'No permission' }); + } + const data = await request.formData(); - const featureString = data.get('feature') as string; - const dataset: DatasetFeature = JSON.parse(featureString); + const featureString = data.get('feature') as string; + const dataset: DatasetFeature = JSON.parse(featureString); - const user_email = session?.user.email; - const now = new Date().toISOString(); - if (!dataset.properties.created_user) { - dataset.properties.created_user = user_email; - dataset.properties.createdat = now; - } - dataset.properties.updated_user = user_email; - dataset.properties.updatedat = now; + const user_email = session?.user.email; + const now = new Date().toISOString(); + if (!dataset.properties.created_user) { + dataset.properties.created_user = user_email; + dataset.properties.createdat = now; + } + dataset.properties.updated_user = user_email; + dataset.properties.updatedat = now; - await upsertDataset(dataset); + const dsManager = new DatasetManager(dataset); + await dsManager.upsert(); - return dataset; - } catch (error) { - return fail(500, { status: error.status, message: 'error:' + error.message }); - } + return dataset; } } satisfies Actions; diff --git a/sites/geohub/src/routes/(app)/management/stac/catalog/[id]/+page.server.ts b/sites/geohub/src/routes/(app)/management/stac/catalog/[id]/+page.server.ts index 178d6ee8c..a8794e9d0 100644 --- a/sites/geohub/src/routes/(app)/management/stac/catalog/[id]/+page.server.ts +++ b/sites/geohub/src/routes/(app)/management/stac/catalog/[id]/+page.server.ts @@ -1,9 +1,10 @@ import type { PageServerLoad } from './$types'; -import { getSTAC, upsertDataset } from '$lib/server/helpers'; +import { getSTAC } from '$lib/server/helpers'; import type { DatasetFeature } from '$lib/types'; import { fail, type Actions, error } from '@sveltejs/kit'; import { generateHashKey } from '$lib/helper'; import { env } from '$env/dynamic/private'; +import DatasetManager from '$lib/server/DatasetManager'; export const load: PageServerLoad = async ({ params, fetch }) => { const id = params.id; @@ -36,30 +37,28 @@ export const load: PageServerLoad = async ({ params, fetch }) => { export const actions = { register: async (event) => { const { request, locals } = event; - try { - const session = await locals.auth(); - if (!session) { - return fail(403, { message: 'No permission' }); - } - const data = await request.formData(); - const featureString = data.get('feature') as string; - const dataset: DatasetFeature = JSON.parse(featureString); - - const user_email = session?.user.email; - const now = new Date().toISOString(); - if (!dataset.properties.created_user) { - dataset.properties.created_user = user_email; - dataset.properties.createdat = now; - } - dataset.properties.updated_user = user_email; - dataset.properties.updatedat = now; + const session = await locals.auth(); + if (!session) { + return fail(403, { message: 'No permission' }); + } + const data = await request.formData(); - await upsertDataset(dataset); + const featureString = data.get('feature') as string; + const dataset: DatasetFeature = JSON.parse(featureString); - return dataset; - } catch (error) { - return fail(500, { status: error.status, message: 'error:' + error.message }); + const user_email = session?.user.email; + const now = new Date().toISOString(); + if (!dataset.properties.created_user) { + dataset.properties.created_user = user_email; + dataset.properties.createdat = now; } + dataset.properties.updated_user = user_email; + dataset.properties.updatedat = now; + + const dsManager = new DatasetManager(dataset); + await dsManager.upsert(); + + return dataset; } } satisfies Actions; diff --git a/sites/geohub/src/routes/(app)/maps/[id]/+page.server.ts b/sites/geohub/src/routes/(app)/maps/[id]/+page.server.ts index eb2a68667..a6fcd8874 100644 --- a/sites/geohub/src/routes/(app)/maps/[id]/+page.server.ts +++ b/sites/geohub/src/routes/(app)/maps/[id]/+page.server.ts @@ -1,4 +1,4 @@ -import type { PageServerLoad } from '../../../(map)/maps/[id]/$types'; +import type { PageServerLoad } from './$types'; import { getStyleById } from '$lib/server/helpers'; import { error } from '@sveltejs/kit'; import type { DashboardMapStyle } from '$lib/types'; diff --git a/sites/geohub/src/routes/+layout.server.ts b/sites/geohub/src/routes/+layout.server.ts index f77b5283e..77102fb7c 100644 --- a/sites/geohub/src/routes/+layout.server.ts +++ b/sites/geohub/src/routes/+layout.server.ts @@ -14,13 +14,11 @@ export const load: LayoutServerLoad = async ({ locals, url }) => { geohubApi = env.GEOHUB_API_ENDPOINT; } - if (session?.user?.email && url.origin.indexOf('localhost') === -1) { + let is_superuser = false; + if (session?.user?.email) { // if not localhost, store signed up user email to database. If not first time visit, update last accessed time column await upsertUser(session.user.email); - } - let is_superuser = false; - if (session?.user?.email) { is_superuser = await isSuperuser(session.user.email); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore diff --git a/sites/geohub/src/routes/api/continents/+server.ts b/sites/geohub/src/routes/api/continents/+server.ts index ea7af6bc3..62717ce09 100644 --- a/sites/geohub/src/routes/api/continents/+server.ts +++ b/sites/geohub/src/routes/api/continents/+server.ts @@ -1,40 +1,41 @@ import type { RequestHandler } from './$types'; -import DatabaseManager from '$lib/server/DatabaseManager'; +import { db } from '$lib/server/db'; +import { countryInGeohub, tagInGeohub } from '$lib/server/schema'; +import { sql } from 'drizzle-orm'; /** * Continent API * return continent data */ export const GET: RequestHandler = async ({ url }) => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - let isTagFilter = false; - const filterByTag = url.searchParams.get('filterbytag'); - if (filterByTag && filterByTag === 'true') { - isTagFilter = true; - } + let isTagFilter = false; + const filterByTag = url.searchParams.get('filterbytag'); + if (filterByTag && filterByTag === 'true') { + isTagFilter = true; + } + + const query = sql` + SELECT + ${countryInGeohub.region1Code} as continent_code, + ${countryInGeohub.region1Name} as continent_name + FROM ${countryInGeohub}`; - const sql = { - text: ` - SELECT region1_code as continent_code, region1_name as continent_name - FROM geohub.country - ${ - isTagFilter - ? `WHERE EXISTS (select id FROM geohub.tag WHERE key='continent' and value=region1_name)` - : '' - } - GROUP BY region1_code, region1_name - ORDER BY region1_name` - }; - // console.log(sql) - const res = await client.query(sql); - return new Response(JSON.stringify(res.rows)); - } catch (err) { - return new Response(JSON.stringify({ message: err.message }), { - status: 400 - }); - } finally { - dbm.end(); + if (isTagFilter) { + query.append( + sql`WHERE EXISTS ( + SELECT ${tagInGeohub.id} + FROM ${tagInGeohub} + WHERE ${tagInGeohub.key}='continent' + AND ${tagInGeohub.value}=${countryInGeohub.region1Name} + )` + ); } + query.append(sql` + GROUP BY ${countryInGeohub.region1Code}, ${countryInGeohub.region1Name} + ORDER BY ${countryInGeohub.region1Name} + `); + + const continents = await db.execute(query); + + return new Response(JSON.stringify(continents)); }; diff --git a/sites/geohub/src/routes/api/countries/+server.ts b/sites/geohub/src/routes/api/countries/+server.ts index e05979e08..f3fbc1fea 100644 --- a/sites/geohub/src/routes/api/countries/+server.ts +++ b/sites/geohub/src/routes/api/countries/+server.ts @@ -1,73 +1,76 @@ import type { RequestHandler } from './$types'; -import DatabaseManager from '$lib/server/DatabaseManager'; +import { sql } from 'drizzle-orm'; +import { countryInGeohub, tagInGeohub } from '$lib/server/schema'; +import { db } from '$lib/server/db'; /** * Country API * return country data */ export const GET: RequestHandler = async ({ url }) => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); const continent_code = url.searchParams.get('continent'); const region_code = url.searchParams.get('region'); - try { - const values = []; - const wheres: string[] = []; + + let isTagFilter = false; + const filterByTag = url.searchParams.get('filterbytag'); + if (filterByTag && filterByTag === 'true') { + isTagFilter = true; + } + + const query = sql` + SELECT + ${countryInGeohub.iso3} as iso_3, + ${countryInGeohub.isoCode} as iso_code, + ${countryInGeohub.iso2} as iso_2, + ${countryInGeohub.name} as country_name, + ${countryInGeohub.region2Code} as region_code, + ${countryInGeohub.region2Name} as region_name, + ${countryInGeohub.region1Code} as continent_code, + ${countryInGeohub.region1Name} as continent_name + FROM ${countryInGeohub}`; + + if (continent_code || region_code) { + query.append(sql`WHERE`); + } + + if (continent_code) { + query.append(sql`${countryInGeohub.region1Code}=${continent_code} `); + } + if (region_code) { if (continent_code) { - values.push(continent_code); - wheres.push(` region1_code=$${values.length} `); - } - if (region_code) { - values.push(region_code); - wheres.push(` region2_code=$${values.length} `); + query.append(sql`AND`); } + query.append(sql`${countryInGeohub.region2Code}=${region_code} `); + } - let isTagFilter = false; - const filterByTag = url.searchParams.get('filterbytag'); - if (filterByTag && filterByTag === 'true') { - isTagFilter = true; + if (isTagFilter) { + if (continent_code || region_code) { + query.append(sql`AND `); + } else { + query.append(sql`WHERE `); } - const sql = { - text: ` - SELECT - iso_3, - iso_code, - iso_2, - name as country_name, - region2_code as region_code, - region2_name as region_name, - region1_code as continent_code, - region1_name as continent_name - FROM geohub.country - ${continent_code || region_code ? `WHERE ${wheres.join(' AND ')}` : ''} - ${ - isTagFilter - ? `${ - continent_code || region_code ? `AND` : 'WHERE' - } EXISTS (select id FROM geohub.tag WHERE key='country' and value=iso_3)` - : '' - } - GROUP BY - iso_3, - iso_code, - iso_2, - name, - region2_code, - region2_name, - region1_code, - region1_name - ORDER BY name`, - values: values - }; - // console.log(sql) - const res = await client.query(sql); - return new Response(JSON.stringify(res.rows)); - } catch (err) { - return new Response(JSON.stringify({ message: err.message }), { - status: 400 - }); - } finally { - dbm.end(); + query.append(sql`EXISTS ( + SELECT ${tagInGeohub.id} + FROM ${tagInGeohub} + WHERE ${tagInGeohub.key}='country' + AND ${tagInGeohub.value}=iso_3)`); } + + query.append(sql` + GROUP BY + ${countryInGeohub.iso3}, + ${countryInGeohub.isoCode}, + ${countryInGeohub.iso2}, + ${countryInGeohub.name}, + ${countryInGeohub.region2Code}, + ${countryInGeohub.region2Name}, + ${countryInGeohub.region1Code}, + ${countryInGeohub.region1Name} + ORDER BY ${countryInGeohub.name} + `); + + const countries = await db.execute(query); + + return new Response(JSON.stringify(countries)); }; diff --git a/sites/geohub/src/routes/api/datasets/+server.ts b/sites/geohub/src/routes/api/datasets/+server.ts index 3e5f722d7..36daaf1aa 100644 --- a/sites/geohub/src/routes/api/datasets/+server.ts +++ b/sites/geohub/src/routes/api/datasets/+server.ts @@ -1,20 +1,20 @@ import type { RequestHandler } from './$types'; -import type { PoolClient } from 'pg'; import type { DatasetFeatureCollection, Pages, Link, DatasetFeature, Tag } from '$lib/types'; import { createDatasetSearchWhereExpression } from '$lib/server/helpers/createDatasetSearchWhereExpression'; import { createDatasetLinks, pageNumber, isSuperuser, - upsertDataset, generateAzureBlobSasToken, getDatasetById } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { Permission } from '$lib/config/AppConfig'; import { env } from '$env/dynamic/private'; import { error } from '@sveltejs/kit'; import { removeSasTokenFromDatasetUrl } from '$lib/helper'; +import DatasetManager from '$lib/server/DatasetManager'; +import { SQL, sql } from 'drizzle-orm'; +import { db } from '$lib/server/db'; /** * Datasets search API @@ -38,60 +38,55 @@ export const GET: RequestHandler = async ({ url, locals }) => { const session = await locals.auth(); const user_email = session?.user.email; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const _limit = url.searchParams.get('limit') || 10; - const limit = Number(_limit); - const _offset = url.searchParams.get('offset') || 0; - const offset = Number(_offset); - - const sortby = url.searchParams.get('sortby'); - let sortByColumn = 'name'; - let SortOrder: 'asc' | 'desc' = 'asc'; - if (sortby) { - const values = sortby.split(','); - const column: string = values[0].trim().toLowerCase(); - const targetSortingColumns = ['name', 'license', 'createdat', 'updatedat', 'no_stars']; - const targetSortingOrder = ['asc', 'desc']; - if (!targetSortingColumns.includes(column)) { - console.log(targetSortingColumns, column); + const _limit = url.searchParams.get('limit') || 10; + const limit = Number(_limit); + const _offset = url.searchParams.get('offset') || 0; + const offset = Number(_offset); + + const sortby = url.searchParams.get('sortby'); + let sortByColumn = 'name'; + let SortOrder: 'asc' | 'desc' = 'asc'; + if (sortby) { + const values = sortby.split(','); + const column: string = values[0].trim().toLowerCase(); + const targetSortingColumns = ['name', 'license', 'createdat', 'updatedat', 'no_stars']; + const targetSortingOrder = ['asc', 'desc']; + if (!targetSortingColumns.includes(column)) { + console.log(targetSortingColumns, column); + error(400, { + message: `Bad parameter for 'sortby'. It must be one of '${targetSortingColumns.join( + ', ' + )}'` + }); + } + sortByColumn = column; + + if (values.length > 1) { + const order: string = values[1].trim().toLowerCase(); + if (!targetSortingOrder.includes(order)) { error(400, { - message: `Bad parameter for 'sortby'. It must be one of '${targetSortingColumns.join( + message: `Bad parameter for 'sortby'. Sorting order must be one of '${targetSortingOrder.join( ', ' )}'` }); } - sortByColumn = column; - - if (values.length > 1) { - const order: string = values[1].trim().toLowerCase(); - if (!targetSortingOrder.includes(order)) { - error(400, { - message: `Bad parameter for 'sortby'. Sorting order must be one of '${targetSortingOrder.join( - ', ' - )}'` - }); - } - SortOrder = order as 'asc' | 'desc'; - } + SortOrder = order as 'asc' | 'desc'; } + } - let is_superuser = false; - if (user_email) { - is_superuser = await isSuperuser(user_email); - } + let is_superuser = false; + if (user_email) { + is_superuser = await isSuperuser(user_email); + } - const whereExpressesion = await createDatasetSearchWhereExpression( - url, - 'x', - is_superuser, - user_email - ); - const values = whereExpressesion.values; + const whereExpressesion = await createDatasetSearchWhereExpression( + url, + 'x', + is_superuser, + user_email + ); - const sql = { - text: ` + const mainSql = sql.raw(` WITH datasetTags as ( SELECT x.id, @@ -180,92 +175,95 @@ export const GET: RequestHandler = async ({ url, locals }) => { ` : '' } - ${whereExpressesion.sql} - ORDER BY - ${sortByColumn} ${SortOrder} NULLS ${SortOrder === 'asc' ? 'FIRST' : 'LAST'} - LIMIT ${limit} - OFFSET ${offset} - ) AS feature - ) AS featurecollection - `, - values: values - }; - // console.log(sql); - const res = await client.query(sql); - const geojson: DatasetFeatureCollection = res.rows[0].geojson; - if (!geojson.features) { - geojson.features = []; - } + `); - const nextUrl = new URL(url.toString()); - nextUrl.searchParams.set('limit', limit.toString()); - nextUrl.searchParams.set('offset', (offset + limit).toString()); - const links: Link[] = [ - { - rel: 'root', - type: 'application/json', - href: `${url.origin}${url.pathname}` - }, - { - rel: 'self', - type: 'application/json', - href: url.toString() - } - ]; + const sqlChunks: SQL[] = [mainSql]; + if (whereExpressesion) { + sqlChunks.push(whereExpressesion); + } - if (geojson.features.length === limit) { - links.push({ - rel: 'next', - type: 'application/json', - href: nextUrl.toString() - }); - } + sqlChunks.push( + sql.raw(` + ORDER BY + ${sortByColumn} ${SortOrder} NULLS ${SortOrder === 'asc' ? 'FIRST' : 'LAST'} + LIMIT ${limit} + OFFSET ${offset} + ) AS feature + ) AS featurecollection + `) + ); - if (offset > 0) { - const previoustUrl = new URL(url.toString()); - previoustUrl.searchParams.set('limit', limit.toString()); - previoustUrl.searchParams.set('offset', (offset - limit).toString()); + // console.log(sql); + const res = await db.execute(sql.join(sqlChunks, sql.raw(' '))); + const geojson: DatasetFeatureCollection = res[0].geojson as unknown as DatasetFeatureCollection; + if (!geojson.features) { + geojson.features = []; + } - links.push({ - rel: 'previous', - type: 'application/json', - href: previoustUrl.toString() - }); + const nextUrl = new URL(url.toString()); + nextUrl.searchParams.set('limit', limit.toString()); + nextUrl.searchParams.set('offset', (offset + limit).toString()); + const links: Link[] = [ + { + rel: 'root', + type: 'application/json', + href: `${url.origin}${url.pathname}` + }, + { + rel: 'self', + type: 'application/json', + href: url.toString() } + ]; + + if (geojson.features.length === limit) { + links.push({ + rel: 'next', + type: 'application/json', + href: nextUrl.toString() + }); + } - geojson.links = links; + if (offset > 0) { + const previoustUrl = new URL(url.toString()); + previoustUrl.searchParams.set('limit', limit.toString()); + previoustUrl.searchParams.set('offset', (offset - limit).toString()); - const totalCount = await getTotalCount(client, whereExpressesion.sql, values); + links.push({ + rel: 'previous', + type: 'application/json', + href: previoustUrl.toString() + }); + } - let totalPages = Math.ceil(totalCount / Number(limit)); - if (totalPages === 0) { - totalPages = 1; - } - const currentPage = pageNumber(totalCount, Number(limit), Number(offset)); - const pages: Pages = { - totalCount, - totalPages, - currentPage - }; - - if (totalPages === currentPage) { - // remove next link if it is the last page - geojson.links = geojson.links.filter((l) => !['next'].includes(l.rel)); - } + geojson.links = links; - geojson.pages = pages; + const totalCount = await getTotalCount(whereExpressesion); - // add SAS token if it is Azure Blob source - for (const feature of geojson.features) { - feature.properties = await createDatasetLinks(feature, url.origin, env.TITILER_ENDPOINT); - } + let totalPages = Math.ceil(totalCount / Number(limit)); + if (totalPages === 0) { + totalPages = 1; + } + const currentPage = pageNumber(totalCount, Number(limit), Number(offset)); + const pages: Pages = { + totalCount, + totalPages, + currentPage + }; + + if (totalPages === currentPage) { + // remove next link if it is the last page + geojson.links = geojson.links.filter((l) => !['next'].includes(l.rel)); + } + + geojson.pages = pages; - return new Response(JSON.stringify(geojson)); - } catch (err) { - error(400, err); - } finally { - dbm.end(); + // add SAS token if it is Azure Blob source + for (const feature of geojson.features) { + feature.properties = await createDatasetLinks(feature, url.origin, env.TITILER_ENDPOINT); } + + return new Response(JSON.stringify(geojson)); }; export const POST: RequestHandler = async ({ fetch, locals, request }) => { @@ -295,7 +293,7 @@ export const POST: RequestHandler = async ({ fetch, locals, request }) => { error(400, { message: 'description property is required' }); } - const tags: Tag[] = body.properties.tags; + const tags: Tag[] = body.properties.tags as Tag[]; if (tags.filter((t) => t.key === 'provider').length === 0) { error(400, 'Data provider is required'); @@ -313,7 +311,9 @@ export const POST: RequestHandler = async ({ fetch, locals, request }) => { body.properties.url = removeSasTokenFromDatasetUrl(body.properties.url); body.properties.url = decodeURI(body.properties.url); - await upsertDataset(body); + const dsManager = new DatasetManager(body); + await dsManager.upsert(); + // if the dataset is under data-upload storage account, delete .ingesting file after registering metadata const dataType = body.properties.tags?.find((t) => t.key === 'type')?.value ?? ''; const azaccount = env.AZURE_STORAGE_ACCOUNT_UPLOAD; @@ -332,28 +332,23 @@ export const POST: RequestHandler = async ({ fetch, locals, request }) => { } } } - const dbm = new DatabaseManager(); - let dataset: DatasetFeature; - try { - const client = await dbm.start(); - dataset = await getDatasetById(client, body.properties.id, is_superuser, user_email); - } finally { - await dbm.end(); - } + const dataset = await getDatasetById(body.properties.id, is_superuser, user_email); + return new Response(JSON.stringify(dataset)); }; -const getTotalCount = async (client: PoolClient, whereSql: string, values: string[]) => { - const sql = { - text: ` - SELECT +const getTotalCount = async (whereSql: SQL) => { + const sqlChunks: SQL[] = [ + sql.raw(` + SELECT COUNT(x.id) as count - FROM geohub.dataset x - ${whereSql} - `, - values: values - }; - const res = await client.query(sql); - const count = Number(res.rows[0]['count']); + FROM geohub.dataset x + `) + ]; + sqlChunks.push(whereSql); + + const res = await db.execute(sql.join(sqlChunks, sql.raw(' '))); + + const count = Number(res[0]['count']); return count; }; diff --git a/sites/geohub/src/routes/api/datasets/[id]/+server.ts b/sites/geohub/src/routes/api/datasets/[id]/+server.ts index 25ec227e1..ca1e72702 100644 --- a/sites/geohub/src/routes/api/datasets/[id]/+server.ts +++ b/sites/geohub/src/routes/api/datasets/[id]/+server.ts @@ -5,7 +5,6 @@ import { getDatasetById, isSuperuser } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; import DatasetManager from '$lib/server/DatasetManager'; import { env } from '$env/dynamic/private'; import { AccessLevel, Permission } from '$lib/config/AppConfig'; @@ -22,49 +21,43 @@ export const GET: RequestHandler = async ({ params, locals, url }) => { const id = params.id; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const dataset = await getDatasetById(client, id, is_superuser, user_email); - if (!dataset) { - return new Response(JSON.stringify({ message: `No dataset found.` }), { - status: 404 - }); - } + const dataset = await getDatasetById(id, is_superuser, user_email); + if (!dataset) { + return new Response(JSON.stringify({ message: `No dataset found.` }), { + status: 404 + }); + } - if (!is_superuser) { - const dp = new DatasetPermissionManager(id, user_email); - const permission = await dp.getBySignedUser(client); - if (!(permission && permission >= Permission.READ)) { - const domain = user_email ? getDomainFromEmail(user_email) : undefined; - const access_level: AccessLevel = dataset.properties.access_level; - if (access_level === AccessLevel.PRIVATE) { - if (dataset.properties.created_user !== user_email) { - return new Response( - JSON.stringify({ message: `No permission to access to this dataset.` }), - { - status: 403 - } - ); - } - } else if (access_level === AccessLevel.ORGANIZATION) { - if (!dataset.properties.created_user.endsWith(domain)) { - return new Response( - JSON.stringify({ message: `No permission to access to this dataset.` }), - { - status: 403 - } - ); - } + if (!is_superuser) { + const dp = new DatasetPermissionManager(id, user_email); + const permission = await dp.getBySignedUser(); + if (!(permission && permission >= Permission.READ)) { + const domain = user_email ? getDomainFromEmail(user_email) : undefined; + const access_level: AccessLevel = dataset.properties.access_level; + if (access_level === AccessLevel.PRIVATE) { + if (dataset.properties.created_user !== user_email) { + return new Response( + JSON.stringify({ message: `No permission to access to this dataset.` }), + { + status: 403 + } + ); + } + } else if (access_level === AccessLevel.ORGANIZATION) { + if (!dataset.properties.created_user.endsWith(domain)) { + return new Response( + JSON.stringify({ message: `No permission to access to this dataset.` }), + { + status: 403 + } + ); } } } - - dataset.properties = await createDatasetLinks(dataset, url.origin, env.TITILER_ENDPOINT); - return new Response(JSON.stringify(dataset)); - } finally { - dbm.end(); } + + dataset.properties = await createDatasetLinks(dataset, url.origin, env.TITILER_ENDPOINT); + return new Response(JSON.stringify(dataset)); }; export const DELETE: RequestHandler = async ({ params, locals }) => { @@ -82,61 +75,52 @@ export const DELETE: RequestHandler = async ({ params, locals }) => { const id = params.id; - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - try { - const dataset = await getDatasetById(client, id, is_superuser, user_email); - if (!dataset) { - return new Response(JSON.stringify({ message: `No dataset found.` }), { - status: 404 - }); - } + const dataset = await getDatasetById(id, is_superuser, user_email); + if (!dataset) { + return new Response(JSON.stringify({ message: `No dataset found.` }), { + status: 404 + }); + } - if (!(dataset.properties.permission === Permission.OWNER)) { - return new Response( - JSON.stringify({ message: `You don't have permission to delete this datasets.` }), - { - status: 403 - } - ); - } + if (!(dataset.properties.permission === Permission.OWNER)) { + return new Response( + JSON.stringify({ message: `You don't have permission to delete this datasets.` }), + { + status: 403 + } + ); + } - const dsm = new DatasetManager(dataset); - await dsm.delete(client, dataset.properties.id); + const dsm = new DatasetManager(dataset); + await dsm.delete(dataset.properties.id as string); - const azaccount = env.AZURE_STORAGE_ACCOUNT_UPLOAD; - const dataType = dataset.properties.tags?.find((t) => t.key === 'type')?.value ?? ''; - if (dataType === 'azure' && dataset.properties.url.indexOf(azaccount) > -1) { - const blobServiceClient = getBlobServiceClient( - env.AZURE_STORAGE_ACCOUNT_UPLOAD, - env.AZURE_STORAGE_ACCESS_KEY_UPLOAD + const azaccount = env.AZURE_STORAGE_ACCOUNT_UPLOAD; + const dataType = dataset.properties.tags?.find((t) => t.key === 'type')?.value ?? ''; + if (dataType === 'azure' && dataset.properties.url.indexOf(azaccount) > -1) { + const blobServiceClient = getBlobServiceClient( + env.AZURE_STORAGE_ACCOUNT_UPLOAD, + env.AZURE_STORAGE_ACCESS_KEY_UPLOAD + ); + const containerName = 'userdata'; + const userHash = session.user.id; + if (dataset.properties.url.indexOf(`${containerName}/${userHash}/datasets`) !== -1) { + // only generate .ingesting file if the file is under /userdata/{id}/raw folder + const containerClient = blobServiceClient.getContainerClient(containerName); + let blobName = dataset.properties.url + .replace('pmtiles://', '') + .replace(`${containerClient.url}/`, '') + .split('?')[0]; + blobName = `${blobName}.ingesting`; + const blockBlobClient = await containerClient.getBlockBlobClient(blobName); + const uploadBlobResponse = await blockBlobClient.upload('', 0); + console.log( + `Upload .ingesting file (${blobName}) successfully`, + uploadBlobResponse.requestId ); - const containerName = 'userdata'; - const userHash = session.user.id; - if (dataset.properties.url.indexOf(`${containerName}/${userHash}/datasets`) !== -1) { - // only generate .ingesting file if the file is under /userdata/{id}/raw folder - const containerClient = blobServiceClient.getContainerClient(containerName); - let blobName = dataset.properties.url - .replace('pmtiles://', '') - .replace(`${containerClient.url}/`, '') - .split('?')[0]; - blobName = `${blobName}.ingesting`; - const blockBlobClient = await containerClient.getBlockBlobClient(blobName); - const uploadBlobResponse = await blockBlobClient.upload('', 0); - console.log( - `Upload .ingesting file (${blobName}) successfully`, - uploadBlobResponse.requestId - ); - } } - - return new Response(undefined, { - status: 204 - }); - } catch (err) { - await dbm.transactionRollback(); - throw err; - } finally { - await dbm.transactionEnd(); } + + return new Response(undefined, { + status: 204 + }); }; diff --git a/sites/geohub/src/routes/api/datasets/[id]/permission/+server.ts b/sites/geohub/src/routes/api/datasets/[id]/permission/+server.ts index 3e485038c..42a23f339 100644 --- a/sites/geohub/src/routes/api/datasets/[id]/permission/+server.ts +++ b/sites/geohub/src/routes/api/datasets/[id]/permission/+server.ts @@ -1,39 +1,38 @@ import type { RequestHandler } from './$types'; -import { getDatasetById, isSuperuser } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { error } from '@sveltejs/kit'; import { DatasetPermissionManager, type DatasetPermission } from '$lib/server/DatasetPermissionManager'; +import { db } from '$lib/server/db'; +import { eq } from 'drizzle-orm'; +import { datasetInGeohub } from '$lib/server/schema'; + +const datasetExists = async (id: string) => { + const ds = await db + .select({ id: datasetInGeohub.id }) + .from(datasetInGeohub) + .where(eq(datasetInGeohub.id, id)); + return ds && ds.length > 0; +}; export const GET: RequestHandler = async ({ params, locals }) => { const session = await locals.auth(); if (!session) error(403, { message: 'Permission error' }); const user_email = session?.user.email; - let is_superuser = false; - if (user_email) { - is_superuser = await isSuperuser(user_email); - } const id = params.id; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const dataset = await getDatasetById(client, id, is_superuser, user_email); - if (!dataset) { - error(404, { message: `No dataset found.` }); - } + const exists = await datasetExists(id); + if (!exists) { + error(404, { message: `No dataset found.` }); + } - const dpm = new DatasetPermissionManager(id, user_email); - const permissions = await dpm.getAll(client); + const dpm = new DatasetPermissionManager(id, user_email); + const permissions = await dpm.getAll(); - return new Response(JSON.stringify(permissions)); - } finally { - dbm.end(); - } + return new Response(JSON.stringify(permissions)); }; export const POST: RequestHandler = async ({ params, locals, request }) => { @@ -41,10 +40,6 @@ export const POST: RequestHandler = async ({ params, locals, request }) => { if (!session) error(403, { message: 'Permission error' }); const user_email = session?.user.email; - let is_superuser = false; - if (user_email) { - is_superuser = await isSuperuser(user_email); - } const id = params.id; @@ -68,22 +63,16 @@ export const POST: RequestHandler = async ({ params, locals, request }) => { permission: body.permission }; - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - try { - const dataset = await getDatasetById(client, id, is_superuser, user_email); - if (!dataset) { - error(404, { message: `No dataset found.` }); - } - - const dpm = new DatasetPermissionManager(id, user_email); - await dpm.register(client, dataset_permission); - const permissions = await dpm.getAll(client); - - return new Response(JSON.stringify(permissions)); - } finally { - dbm.transactionEnd(); + const exists = await datasetExists(id); + if (!exists) { + error(404, { message: `No dataset found.` }); } + + const dpm = new DatasetPermissionManager(id, user_email); + await dpm.register(dataset_permission); + const permissions = await dpm.getAll(); + + return new Response(JSON.stringify(permissions)); }; export const PUT: RequestHandler = async ({ params, locals, request }) => { @@ -91,10 +80,6 @@ export const PUT: RequestHandler = async ({ params, locals, request }) => { if (!session) error(403, { message: 'Permission error' }); const user_email = session?.user.email; - let is_superuser = false; - if (user_email) { - is_superuser = await isSuperuser(user_email); - } const id = params.id; @@ -122,22 +107,16 @@ export const PUT: RequestHandler = async ({ params, locals, request }) => { createdat: body.createdat }; - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - try { - const dataset = await getDatasetById(client, id, is_superuser, user_email); - if (!dataset) { - error(404, { message: `No dataset found.` }); - } - - const dpm = new DatasetPermissionManager(id, user_email); - await dpm.update(client, dataset_permission); - const permissions = await dpm.getAll(client); - - return new Response(JSON.stringify(permissions)); - } finally { - dbm.transactionEnd(); + const exists = await datasetExists(id); + if (!exists) { + error(404, { message: `No dataset found.` }); } + + const dpm = new DatasetPermissionManager(id, user_email); + await dpm.update(dataset_permission); + const permissions = await dpm.getAll(); + + return new Response(JSON.stringify(permissions)); }; export const DELETE: RequestHandler = async ({ params, locals, url }) => { @@ -145,10 +124,6 @@ export const DELETE: RequestHandler = async ({ params, locals, url }) => { if (!session) error(403, { message: 'Permission error' }); const user_email = session?.user.email; - let is_superuser = false; - if (user_email) { - is_superuser = await isSuperuser(user_email); - } const id = params.id; const target_email = url.searchParams.get('user_email'); @@ -156,20 +131,14 @@ export const DELETE: RequestHandler = async ({ params, locals, url }) => { error(400, { message: `query parameter of user_email is required.` }); } - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - try { - const dataset = await getDatasetById(client, id, is_superuser, user_email); - if (!dataset) { - error(404, { message: `No dataset found.` }); - } - - const dpm = new DatasetPermissionManager(id, user_email); - await dpm.delete(client, target_email); - const permissions = await dpm.getAll(client); - - return new Response(JSON.stringify(permissions)); - } finally { - dbm.transactionEnd(); + const exists = await datasetExists(id); + if (!exists) { + error(404, { message: `No dataset found.` }); } + + const dpm = new DatasetPermissionManager(id, user_email); + await dpm.delete(target_email); + const permissions = await dpm.getAll(); + + return new Response(JSON.stringify(permissions)); }; diff --git a/sites/geohub/src/routes/api/datasets/[id]/preview/style.json/+server.ts b/sites/geohub/src/routes/api/datasets/[id]/preview/style.json/+server.ts index 9165a5281..9572d80e4 100644 --- a/sites/geohub/src/routes/api/datasets/[id]/preview/style.json/+server.ts +++ b/sites/geohub/src/routes/api/datasets/[id]/preview/style.json/+server.ts @@ -1,6 +1,5 @@ import type { RequestHandler } from './$types'; import { createDatasetLinks, getDatasetById, isSuperuser } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { env } from '$env/dynamic/private'; import type { DatasetDefaultLayerStyle, VectorLayerTypes, VectorTileMetadata } from '$lib/types'; import { v4 as uuidv4 } from 'uuid'; @@ -24,110 +23,104 @@ export const GET: RequestHandler = async ({ params, locals, url, fetch }) => { | VectorLayerTypes | 'raster'; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const dataset = await getDatasetById(client, id, is_superuser, user_email); - if (!dataset) { - return new Response(JSON.stringify({ message: `No dataset found.` }), { - status: 404 - }); - } + const dataset = await getDatasetById(id, is_superuser, user_email); + if (!dataset) { + return new Response(JSON.stringify({ message: `No dataset found.` }), { + status: 404 + }); + } - const styleJson: StyleSpecification = { - version: 8, - name: dataset.properties.name, - center: [0, 0], - zoom: 0, - bearing: 0, - pitch: 0, - sources: {}, - layers: [ - { - id: 'background', - type: 'background', - layout: { - visibility: 'visible' - }, - paint: { - 'background-color': '#fbf8f3', - 'background-opacity': 1 - } + const styleJson: StyleSpecification = { + version: 8, + name: dataset.properties.name, + center: [0, 0], + zoom: 0, + bearing: 0, + pitch: 0, + sources: {}, + layers: [ + { + id: 'background', + type: 'background', + layout: { + visibility: 'visible' + }, + paint: { + 'background-color': '#fbf8f3', + 'background-opacity': 1 } - ] - }; - - const isStac = dataset.properties.tags?.find((t) => t.key === 'type')?.value === 'stac'; - if (isStac) { - // if STAC dataset, return the empty style json - return new Response(JSON.stringify(styleJson)); - } - - if (dataset.properties.is_raster) { - // raster - if (!layer_id) { - layer_id = layer_id ?? '1'; - } - layer_type = 'raster'; - } else { - // vector - dataset.properties = await createDatasetLinks(dataset, url.origin, env.TITILER_ENDPOINT); - const metadataJsonUrl = dataset.properties.links?.find((l) => l.rel === 'metadatajson')?.href; - const res = await fetch(metadataJsonUrl); - if (!res.ok) { - error(res.status, res.statusText); } - const metadata: VectorTileMetadata = await res.json(); - if (!layer_id) { - layer_id = metadata.json.vector_layers[0].id; - } - if (!layer_type) { - const tilestats = metadata.json.tilestats.layers.find((l) => l.layer === layer_id); + ] + }; - const geomType = tilestats.geometry.toLowerCase(); - if (geomType === 'point' || geomType === 'multipoint') { - layer_type = 'symbol'; - } else if (geomType === 'linestring' || geomType === 'multilinestring') { - layer_type = 'line'; - } else { - layer_type = 'fill'; - } - } + const isStac = dataset.properties.tags?.find((t) => t.key === 'type')?.value === 'stac'; + if (isStac) { + // if STAC dataset, return the empty style json + return new Response(JSON.stringify(styleJson)); + } - if (['symbol'].includes(layer_type)) { - const resBaseStyle = await fetch(MapStyles[0].uri); - const baseStyle: StyleSpecification = await resBaseStyle.json(); - styleJson.sprite = baseStyle.sprite; - } + if (dataset.properties.is_raster) { + // raster + if (!layer_id) { + layer_id = layer_id ?? '1'; } - - const styleApi = `/api/datasets/${id}/style/${layer_id}/${layer_type}`; - const res = await fetch(styleApi); + layer_type = 'raster'; + } else { + // vector + dataset.properties = await createDatasetLinks(dataset, url.origin, env.TITILER_ENDPOINT); + const metadataJsonUrl = dataset.properties.links?.find((l) => l.rel === 'metadatajson')?.href; + const res = await fetch(metadataJsonUrl); if (!res.ok) { error(res.status, res.statusText); } - const datasetStyle: DatasetDefaultLayerStyle = await res.json(); + const metadata: VectorTileMetadata = await res.json(); + if (!layer_id) { + layer_id = metadata.json.vector_layers[0].id; + } + if (!layer_type) { + const tilestats = metadata.json.tilestats.layers.find((l) => l.layer === layer_id); + + const geomType = tilestats.geometry.toLowerCase(); + if (geomType === 'point' || geomType === 'multipoint') { + layer_type = 'symbol'; + } else if (geomType === 'linestring' || geomType === 'multilinestring') { + layer_type = 'line'; + } else { + layer_type = 'fill'; + } + } + + if (['symbol'].includes(layer_type)) { + const resBaseStyle = await fetch(MapStyles[0].uri); + const baseStyle: StyleSpecification = await resBaseStyle.json(); + styleJson.sprite = baseStyle.sprite; + } + } + + const styleApi = `/api/datasets/${id}/style/${layer_id}/${layer_type}`; + const res = await fetch(styleApi); + if (!res.ok) { + error(res.status, res.statusText); + } + const datasetStyle: DatasetDefaultLayerStyle = await res.json(); - const tileSourceId = dataset.properties.id; - const layerId = uuidv4(); + const tileSourceId = dataset.properties.id; + const layerId = uuidv4(); - const layerSpec = JSON.parse( - JSON.stringify(datasetStyle.style) - .replace('{source_id}', tileSourceId) - .replace('{layer_id}', layerId) - ); - const sourceSpec = JSON.parse(JSON.stringify(datasetStyle.source)); + const layerSpec = JSON.parse( + JSON.stringify(datasetStyle.style) + .replace('{source_id}', tileSourceId) + .replace('{layer_id}', layerId) + ); + const sourceSpec = JSON.parse(JSON.stringify(datasetStyle.source)); - const coordinates = dataset.geometry.coordinates[0] as [number, number][]; - const pos = geoViewport.viewport([...coordinates[0], ...coordinates[2]], [500, 500]); + const coordinates = dataset.geometry.coordinates[0] as [number, number][]; + const pos = geoViewport.viewport([...coordinates[0], ...coordinates[2]], [500, 500]); - styleJson.center = pos.center; - styleJson.zoom = Number.isNaN(pos.zoom) ? 0 : pos.zoom; - styleJson.layers.push(layerSpec); - styleJson.sources[tileSourceId] = sourceSpec; + styleJson.center = pos.center; + styleJson.zoom = Number.isNaN(pos.zoom) ? 0 : pos.zoom; + styleJson.layers.push(layerSpec); + styleJson.sources[tileSourceId] = sourceSpec; - return new Response(JSON.stringify(styleJson)); - } finally { - dbm.end(); - } + return new Response(JSON.stringify(styleJson)); }; diff --git a/sites/geohub/src/routes/api/datasets/[id]/star/+server.ts b/sites/geohub/src/routes/api/datasets/[id]/star/+server.ts index 73034ab78..d2f753384 100644 --- a/sites/geohub/src/routes/api/datasets/[id]/star/+server.ts +++ b/sites/geohub/src/routes/api/datasets/[id]/star/+server.ts @@ -1,6 +1,8 @@ import type { RequestHandler } from './$types'; import { getDatasetStarCount } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; +import { db } from '$lib/server/db'; +import { datasetFavouriteInGeohub } from '$lib/server/schema'; +import { sql } from 'drizzle-orm'; export const POST: RequestHandler = async ({ locals, params }) => { const session = await locals.auth(); @@ -14,46 +16,26 @@ export const POST: RequestHandler = async ({ locals, params }) => { const user_email = session.user.email; const now = new Date().toISOString(); - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: ` - INSERT INTO geohub.dataset_favourite ( - dataset_id, user_email, savedat - ) values ( - $1, - $2, - $3::timestamptz - ) - ON CONFLICT (dataset_id, user_email) - DO - UPDATE - SET - savedat=$3::timestamptz - `, - values: [dataset_id, user_email, now] - }; - - await client.query(query); + await db + .insert(datasetFavouriteInGeohub) + .values({ datasetId: dataset_id, userEmail: user_email, savedat: now }) + .onConflictDoUpdate({ + target: [datasetFavouriteInGeohub.datasetId, datasetFavouriteInGeohub.userEmail], + set: { + savedat: now + } + }); - const stars = await getDatasetStarCount(client, dataset_id); + const stars = await getDatasetStarCount(dataset_id); - const res = { - dataset_id, - user_email, - savedat: now, - no_stars: stars - }; + const res = { + dataset_id, + user_email, + savedat: now, + no_stars: stars + }; - return new Response(JSON.stringify(res)); - } catch (err) { - return new Response(JSON.stringify({ message: err.message }), { - status: 400 - }); - } finally { - dbm.end(); - } + return new Response(JSON.stringify(res)); }; export const DELETE: RequestHandler = async ({ locals, params }) => { @@ -67,29 +49,18 @@ export const DELETE: RequestHandler = async ({ locals, params }) => { const dataset_id = params.id; const user_email = session.user.email; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: `DELETE FROM geohub.dataset_favourite WHERE dataset_id=$1 and user_email=$2`, - values: [dataset_id, user_email] - }; + await db + .delete(datasetFavouriteInGeohub) + .where( + sql`${datasetFavouriteInGeohub.datasetId} = ${dataset_id} AND ${datasetFavouriteInGeohub.userEmail} = ${user_email}` + ); - await client.query(query); + const stars = await getDatasetStarCount(dataset_id); - const stars = await getDatasetStarCount(client, dataset_id); + const res = { + dataset_id, + no_stars: stars + }; - const res = { - dataset_id, - no_stars: stars - }; - - return new Response(JSON.stringify(res)); - } catch (err) { - return new Response(JSON.stringify({ message: err.message }), { - status: 400 - }); - } finally { - dbm.end(); - } + return new Response(JSON.stringify(res)); }; diff --git a/sites/geohub/src/routes/api/datasets/[id]/star/count/+server.ts b/sites/geohub/src/routes/api/datasets/[id]/star/count/+server.ts index a0b8d145f..a9e7e311f 100644 --- a/sites/geohub/src/routes/api/datasets/[id]/star/count/+server.ts +++ b/sites/geohub/src/routes/api/datasets/[id]/star/count/+server.ts @@ -1,26 +1,14 @@ import type { RequestHandler } from './$types'; import { getDatasetStarCount } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; export const GET: RequestHandler = async ({ params }) => { const dataset_id = params.id; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const stars = await getDatasetStarCount(client, dataset_id); + const stars = await getDatasetStarCount(dataset_id); + const res = { + dataset_id, + no_stars: stars + }; - const res = { - dataset_id, - no_stars: stars - }; - - return new Response(JSON.stringify(res)); - } catch (err) { - return new Response(JSON.stringify({ message: err.message }), { - status: 400 - }); - } finally { - dbm.end(); - } + return new Response(JSON.stringify(res)); }; diff --git a/sites/geohub/src/routes/api/datasets/[id]/style/[layer]/[type]/+server.ts b/sites/geohub/src/routes/api/datasets/[id]/style/[layer]/[type]/+server.ts index effd0d76d..5131154d4 100644 --- a/sites/geohub/src/routes/api/datasets/[id]/style/[layer]/[type]/+server.ts +++ b/sites/geohub/src/routes/api/datasets/[id]/style/[layer]/[type]/+server.ts @@ -6,10 +6,8 @@ import { getDefaultLayerStyle, isSuperuser } from '$lib/server/helpers'; -import type { PoolClient } from 'pg'; import type { RequestHandler } from './$types'; import { error } from '@sveltejs/kit'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { VectorLayerTypeValues, type DatasetDefaultLayerStyle, @@ -21,6 +19,9 @@ import type { UserConfig } from '$lib/config/DefaultUserConfig'; import { env } from '$env/dynamic/private'; import VectorDefaultStyle from '$lib/server/defaultStyle/VectorDefaultStyle'; import { DatasetPermissionManager } from '$lib/server/DatasetPermissionManager'; +import { datasetDefaultstyleInGeohub } from '$lib/server/schema'; +import { db } from '$lib/server/db'; +import { sql } from 'drizzle-orm'; export const GET: RequestHandler = async ({ params, locals, url, fetch }) => { const session = await locals.auth(); @@ -28,7 +29,7 @@ export const GET: RequestHandler = async ({ params, locals, url, fetch }) => { const id = params.id; const layer_id = params.layer; const layer_type: VectorLayerTypes | 'raster' = params.type as VectorLayerTypes | 'raster'; - const colormap_name = url.searchParams.get('colormap_name'); + const colormap_name = url.searchParams.get('colormap_name') as string; if (![...VectorLayerTypeValues, 'raster'].includes(layer_type)) { error(404, { @@ -44,75 +45,72 @@ export const GET: RequestHandler = async ({ params, locals, url, fetch }) => { is_superuser = await isSuperuser(user_email); } - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const dataset = await getDataset(client, id, is_superuser, user_email); - dataset.properties = await createDatasetLinks(dataset, url.origin, env.TITILER_ENDPOINT); + const dataset = await getDatasetById(id, is_superuser, user_email); + if (!dataset) { + error(404, { message: `No dataset found.` }); + } + dataset.properties = await createDatasetLinks(dataset, url.origin, env.TITILER_ENDPOINT); - const response = await fetch('/api/settings'); - const config: UserConfig = await response.json(); + const response = await fetch('/api/settings'); + const config: UserConfig = await response.json(); - let data = await getDefaultLayerStyle(client, dataset.properties.id, layer_id, layer_type); - if (!data) { - if (layer_type === 'raster') { - const bandIndex = parseInt(layer_id) - 1; - const rasterDefaultStyle = new RasterDefaultStyle(dataset, config, bandIndex); - data = await rasterDefaultStyle.create(colormap_name); - } else { - const vectorDefaultStyle = new VectorDefaultStyle(dataset, config, layer_id, layer_type); - data = await vectorDefaultStyle.create(colormap_name); - } + let data = await getDefaultLayerStyle(dataset.properties.id as string, layer_id, layer_type); + if (!data) { + if (layer_type === 'raster') { + const bandIndex = parseInt(layer_id) - 1; + const rasterDefaultStyle = new RasterDefaultStyle(dataset, config, bandIndex); + data = await rasterDefaultStyle.create(colormap_name); } else { - const attribution = createAttributionFromTags(dataset.properties.tags); - const src = data.source as VectorSourceSpecification | RasterSourceSpecification; - src.attribution = attribution; + const vectorDefaultStyle = new VectorDefaultStyle(dataset, config, layer_id, layer_type); + data = await vectorDefaultStyle.create(colormap_name); } + } else { + const attribution = createAttributionFromTags(dataset.properties.tags); + const src = data.source as VectorSourceSpecification | RasterSourceSpecification; + src.attribution = attribution; + } - if (layer_type === 'raster') { - // if titiler URL saved in database is different from actual server settings, replace URL origin to env varaible one. - const rasterSource = data.source as RasterSourceSpecification; - const tiles = rasterSource.tiles; - const titilerUrl = new URL(env.TITILER_ENDPOINT); - for (let i = 0; i < tiles.length; i++) { - const url = new URL(tiles[i]); - if (url.origin !== titilerUrl.origin) { - tiles[i] = tiles[i].replace(url.origin, titilerUrl.origin); - } + if (layer_type === 'raster') { + // if titiler URL saved in database is different from actual server settings, replace URL origin to env varaible one. + const rasterSource = data.source as RasterSourceSpecification; + const tiles = rasterSource.tiles; + const titilerUrl = new URL(env.TITILER_ENDPOINT); + for (let i = 0; i < tiles.length; i++) { + const url = new URL(tiles[i]); + if (url.origin !== titilerUrl.origin) { + tiles[i] = tiles[i].replace(url.origin, titilerUrl.origin); } } + } - if (!data.metadata) { - if (layer_type === 'raster') { - const bandIndex = parseInt(layer_id) - 1; - const rasterDefaultStyle = new RasterDefaultStyle(dataset, config, bandIndex); - data.metadata = await rasterDefaultStyle.getMetadata(); - } else { - const vectorDefaultStyle = new VectorDefaultStyle(dataset, config, layer_id, layer_type); - data.metadata = await vectorDefaultStyle.getMetadata(); - } + if (!data.metadata) { + if (layer_type === 'raster') { + const bandIndex = parseInt(layer_id) - 1; + const rasterDefaultStyle = new RasterDefaultStyle(dataset, config, bandIndex); + data.metadata = await rasterDefaultStyle.getMetadata(); + } else { + const vectorDefaultStyle = new VectorDefaultStyle(dataset, config, layer_id, layer_type); + data.metadata = await vectorDefaultStyle.getMetadata(); } + } - const isPgTileServ = - dataset.properties.tags?.find((t) => t.key === 'type')?.value === 'pgtileserv'; - if (isPgTileServ) { - const type = data.source.type; - if (type === 'vector') { - const vectorSource = data.source as VectorSourceSpecification; - if (vectorSource.url) { + const isPgTileServ = + dataset.properties.tags?.find((t) => t.key === 'type')?.value === 'pgtileserv'; + if (isPgTileServ) { + const type = data.source.type; + if (type === 'vector') { + const vectorSource = data.source as VectorSourceSpecification; + if (vectorSource.url) { + const originalUrl = new URL(vectorSource.url); + if (originalUrl.origin !== url.origin) { const originalUrl = new URL(vectorSource.url); - if (originalUrl.origin !== url.origin) { - const originalUrl = new URL(vectorSource.url); - vectorSource.url = `${url.origin}${originalUrl.pathname}${originalUrl.search}`; - } + vectorSource.url = `${url.origin}${originalUrl.pathname}${originalUrl.search}`; } } } - - return new Response(JSON.stringify(data)); - } finally { - dbm.end(); } + + return new Response(JSON.stringify(data)); }; export const POST: RequestHandler = async ({ params, locals, request }) => { @@ -139,112 +137,86 @@ export const POST: RequestHandler = async ({ params, locals, request }) => { is_superuser = await isSuperuser(user_email); } - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const dataset = await getDataset(client, id, is_superuser, user_email); + const dataset = await getDatasetById(id, is_superuser, user_email); + if (!dataset) { + error(404, { message: `No dataset found.` }); + } - if (!is_superuser) { - if (!(dataset.properties.permission && dataset.properties.permission > Permission.READ)) { - const domain = user_email ? getDomainFromEmail(user_email) : undefined; - const access_level: AccessLevel = dataset.properties.access_level; - if (access_level === AccessLevel.PRIVATE) { - if (dataset.properties.created_user !== user_email) { - error(403, { message: `No permission to access to this dataset.` }); - } - } else if (access_level === AccessLevel.ORGANIZATION) { - if (!dataset.properties.created_user.endsWith(domain)) { - error(403, { message: `No permission to access to this dataset.` }); - } + if (!is_superuser) { + if (!(dataset.properties.permission && dataset.properties.permission > Permission.READ)) { + const domain = user_email ? getDomainFromEmail(user_email) : undefined; + const access_level: AccessLevel = dataset.properties.access_level; + if (access_level === AccessLevel.PRIVATE) { + if (dataset.properties.created_user !== user_email) { + error(403, { message: `No permission to access to this dataset.` }); + } + } else if (access_level === AccessLevel.ORGANIZATION) { + if (!dataset.properties.created_user.endsWith(domain)) { + error(403, { message: `No permission to access to this dataset.` }); } } } + } - const body: DatasetDefaultLayerStyle = await request.json(); - const now = new Date().toISOString(); - - const source = body.source; - if (!source) { - error(400, { message: `Source property is required to register.` }); - } - const style = body.style; - if (!body.style) { - error(400, { message: `Style property is required to register.` }); - } - if (style.type !== layer_type) { - error(400, { - message: `Layer type in path param does not match to style object in body.` - }); - } - // replace layer_id and source_id to variable - style.id = '{layer_id}'; - style.source = '{source_id}'; + const body: DatasetDefaultLayerStyle = await request.json(); + const now = new Date().toISOString(); - const colormap_name = body.colormap_name; - const classification_method = body.classification_method; - const classification_method_2 = body.classification_method_2; + const source = body.source; + if (!source) { + error(400, { message: `Source property is required to register.` }); + } + const style = body.style; + if (!body.style) { + error(400, { message: `Style property is required to register.` }); + } + if (style.type !== layer_type) { + error(400, { + message: `Layer type in path param does not match to style object in body.` + }); + } + // replace layer_id and source_id to variable + style.id = '{layer_id}'; + style.source = '{source_id}'; - const query = { - text: ` - INSERT INTO geohub.dataset_defaultstyle - ( - dataset_id, - layer_id, - layer_type, - source, - style, - colormap_name, - classification_method, - classification_method_2, - created_user, - createdat - ) - VALUES ( - $1, - $2, - $3, - $4, - $5, - $6, - $7, - $8, - $9, - $10::timestamptz - ) - ON CONFLICT (dataset_id, layer_id, layer_type) - DO UPDATE - SET - source = $4, - style = $5, - colormap_name = $6, - classification_method = $7, - classification_method_2 = $8, - updated_user = $11, - updatedat = $12::timestamptz - `, - values: [ - dataset.properties.id, - layer_id, - layer_type, - source, - style, - colormap_name, - classification_method, - classification_method_2, - user_email, - now, - user_email, - now - ] - }; + const colormap_name = body.colormap_name; + const classification_method = body.classification_method; + const classification_method_2 = body.classification_method_2; - await client.query(query); + await db + .insert(datasetDefaultstyleInGeohub) + .values({ + datasetId: dataset.properties.id as string, + layerId: layer_id, + layerType: layer_type, + source: source, + style: style, + colormapName: colormap_name, + classificationMethod: classification_method, + classificationMethod2: classification_method_2, + createdUser: user_email, + createdat: now + }) + .onConflictDoUpdate({ + target: [ + datasetDefaultstyleInGeohub.datasetId, + datasetDefaultstyleInGeohub.layerId, + datasetDefaultstyleInGeohub.layerType + ], + set: { + layerId: layer_id, + layerType: layer_type, + source: source, + style: style, + colormapName: colormap_name, + classificationMethod: classification_method, + classificationMethod2: classification_method_2, + updatedUser: user_email, + updatedat: now + } + }); - const data = await getDefaultLayerStyle(client, dataset.properties.id, layer_id, layer_type); - return new Response(JSON.stringify(data)); - } finally { - dbm.end(); - } + const data = await getDefaultLayerStyle(dataset.properties.id as string, layer_id, layer_type); + return new Response(JSON.stringify(data)); }; export const DELETE: RequestHandler = async ({ params, locals }) => { @@ -271,59 +243,36 @@ export const DELETE: RequestHandler = async ({ params, locals }) => { is_superuser = await isSuperuser(user_email); } - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const dataset = await getDataset(client, id, is_superuser, user_email); + const dataset = await getDatasetById(id, is_superuser, user_email); + if (!dataset) { + error(404, { message: `No dataset found.` }); + } - if (!is_superuser) { - const dp = new DatasetPermissionManager(id, user_email); - const permission = await dp.getBySignedUser(client); - if (!(permission && permission > Permission.READ)) { - const domain = user_email ? getDomainFromEmail(user_email) : undefined; - const access_level: AccessLevel = dataset.properties.access_level; - if (access_level === AccessLevel.PRIVATE) { - if (dataset.properties.created_user !== user_email) { - error(403, { message: `No permission to access to this dataset.` }); - } - } else if (access_level === AccessLevel.ORGANIZATION) { - if (!dataset.properties.created_user.endsWith(domain)) { - error(403, { message: `No permission to access to this dataset.` }); - } + if (!is_superuser) { + const dp = new DatasetPermissionManager(id, user_email); + const permission = await dp.getBySignedUser(client); + if (!(permission && permission > Permission.READ)) { + const domain = user_email ? getDomainFromEmail(user_email) : undefined; + const access_level: AccessLevel = dataset.properties.access_level; + if (access_level === AccessLevel.PRIVATE) { + if (dataset.properties.created_user !== user_email) { + error(403, { message: `No permission to access to this dataset.` }); + } + } else if (access_level === AccessLevel.ORGANIZATION) { + if (!dataset.properties.created_user.endsWith(domain)) { + error(403, { message: `No permission to access to this dataset.` }); } } } - - const query = { - text: ` - DELETE FROM geohub.dataset_defaultstyle - WHERE - dataset_id=$1 - AND layer_id=$2 - AND layer_type=$3 - `, - values: [dataset.properties.id, layer_id, layer_type] - }; - - await client.query(query); - - return new Response(undefined, { - status: 204 - }); - } finally { - dbm.end(); } -}; -const getDataset = async ( - client: PoolClient, - id: string, - is_superuser: boolean, - user_email?: string -) => { - const dataset = await getDatasetById(client, id, is_superuser, user_email); - if (!dataset) { - error(404, { message: `No dataset found.` }); - } - return dataset; + await db.delete(datasetDefaultstyleInGeohub).where(sql` + ${datasetDefaultstyleInGeohub.datasetId} = ${dataset.properties.id} + AND ${datasetDefaultstyleInGeohub.layerId}= ${layer_id} + AND ${datasetDefaultstyleInGeohub.layerType}= ${layer_type} + `); + + return new Response(undefined, { + status: 204 + }); }; diff --git a/sites/geohub/src/routes/api/licenses/+server.ts b/sites/geohub/src/routes/api/licenses/+server.ts index 36f82a20f..9fe49bd27 100644 --- a/sites/geohub/src/routes/api/licenses/+server.ts +++ b/sites/geohub/src/routes/api/licenses/+server.ts @@ -1,22 +1,12 @@ -import { type RequestHandler, error } from '@sveltejs/kit'; -import DatabaseManager from '$lib/server/DatabaseManager'; +import { type RequestHandler } from '@sveltejs/kit'; import type { License } from '$lib/types'; +import { db } from '$lib/server/db'; +import { asc } from 'drizzle-orm'; +import { licenseInGeohub } from '$lib/server/schema'; export const GET: RequestHandler = async () => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: `SELECT id, name FROM geohub.license ORDER BY license asc` - }; - const res = await client?.query(query); - - const data: License[] = res?.rows as License[]; - - return new Response(JSON.stringify(data)); - } catch (err) { - error(500, err); - } finally { - await dbm.end(); - } + const licenses: License[] = await db.query.licenseInGeohub.findMany({ + orderBy: [asc(licenseInGeohub.name)] + }); + return new Response(JSON.stringify(licenses)); }; diff --git a/sites/geohub/src/routes/api/products/+server.ts b/sites/geohub/src/routes/api/products/+server.ts index 942407bbf..7ec4be0d4 100644 --- a/sites/geohub/src/routes/api/products/+server.ts +++ b/sites/geohub/src/routes/api/products/+server.ts @@ -1,25 +1,20 @@ import { error, type RequestHandler } from '@sveltejs/kit'; import { isSuperuser } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { ProductManager } from '$lib/server/Product'; +import { db } from '$lib/server/db'; +import { productInGeohub } from '$lib/server/schema'; export const GET: RequestHandler = async () => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); + const products = await db + .select({ + id: productInGeohub.id, + description: productInGeohub.description, + expression: productInGeohub.expression, + label: productInGeohub.label + }) + .from(productInGeohub); - try { - const query = { - text: `SELECT id, description, expression, label FROM geohub.product` - }; - const res = await client.query(query); - - const products = res.rows; - return new Response(JSON.stringify(products)); - } catch (err) { - error(500, err); - } finally { - await dbm.end(); - } + return new Response(JSON.stringify(products)); }; export const POST: RequestHandler = async ({ locals, request }) => { @@ -37,18 +32,8 @@ export const POST: RequestHandler = async ({ locals, request }) => { error(403, { message: 'Permission error' }); } const { id, description, expression, label } = await request.json(); - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); const pm = new ProductManager(id, description, expression, label); - - try { - const product = await pm.insert(client); - return new Response(JSON.stringify(product)); - } catch (err) { - await dbm.transactionRollback(); - throw err; - } finally { - await dbm.transactionEnd(); - } + const product = await pm.insert(); + return new Response(JSON.stringify(product)); }; diff --git a/sites/geohub/src/routes/api/products/[id]/+server.ts b/sites/geohub/src/routes/api/products/[id]/+server.ts index d4bd116d1..378f02e6d 100644 --- a/sites/geohub/src/routes/api/products/[id]/+server.ts +++ b/sites/geohub/src/routes/api/products/[id]/+server.ts @@ -1,23 +1,16 @@ import { error, type RequestHandler } from '@sveltejs/kit'; import { isSuperuser } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { ProductManager } from '$lib/server/Product'; export const GET: RequestHandler = async ({ params }) => { const id = params.id; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - const pm = new ProductManager(id); - try { - const product = await pm.get(client); - if (!product) { - error(404, { message: `does not exist in the database` }); - } - return new Response(JSON.stringify(product)); - } finally { - await dbm.end(); + const pm = new ProductManager(id as string); + const product = await pm.get(); + if (!product) { + error(404, { message: `does not exist in the database` }); } + return new Response(JSON.stringify(product)); }; export const PUT: RequestHandler = async ({ locals, request, params }) => { @@ -34,24 +27,16 @@ export const PUT: RequestHandler = async ({ locals, request, params }) => { error(403, { message: 'Permission error' }); } const { description, expression, label } = await request.json(); - const id = params.id; - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - const pm = new ProductManager(id, description, expression, label); - try { - let product = await pm.get(client); - if (!product) { - error(404, { message: `Does not exist in the database` }); - } - product = await pm.update(client); + const id = params.id as string; - return new Response(JSON.stringify(product)); - } catch (err) { - await dbm.transactionRollback(); - throw err; - } finally { - await dbm.transactionEnd(); + const pm = new ProductManager(id, description, expression, label); + let product = await pm.get(); + if (!product) { + error(404, { message: `Does not exist in the database` }); } + product = await pm.update(); + + return new Response(JSON.stringify(product)); }; export const DELETE: RequestHandler = async ({ locals, params }) => { @@ -69,24 +54,16 @@ export const DELETE: RequestHandler = async ({ locals, params }) => { } const id = params.id; - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - const pm = new ProductManager(id); - try { - const product = await pm.get(client); - if (!product) { - error(404, { message: `Does not exist in the database` }); - } - await pm.delete(client); + const pm = new ProductManager(id as string); - return new Response(undefined, { - status: 204 - }); - } catch (err) { - dbm.transactionRollback(); - throw err; - } finally { - await dbm.transactionEnd(); + const product = await pm.get(); + if (!product) { + error(404, { message: `Does not exist in the database` }); } + await pm.delete(); + + return new Response(undefined, { + status: 204 + }); }; diff --git a/sites/geohub/src/routes/api/regions/+server.ts b/sites/geohub/src/routes/api/regions/+server.ts index c5706b105..2dae4c52e 100644 --- a/sites/geohub/src/routes/api/regions/+server.ts +++ b/sites/geohub/src/routes/api/regions/+server.ts @@ -1,48 +1,54 @@ import type { RequestHandler } from './$types'; -import DatabaseManager from '$lib/server/DatabaseManager'; +import { countryInGeohub, tagInGeohub } from '$lib/server/schema'; +import { sql } from 'drizzle-orm'; +import { db } from '$lib/server/db'; /** * Region API * return region data */ export const GET: RequestHandler = async ({ url }) => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); const continent_code = url.searchParams.get('continent'); - try { - const values = []; - if (continent_code) { - values.push(continent_code); - } - let isTagFilter = false; - const filterByTag = url.searchParams.get('filterbytag'); - if (filterByTag && filterByTag === 'true') { - isTagFilter = true; - } + let isTagFilter = false; + const filterByTag = url.searchParams.get('filterbytag'); + if (filterByTag && filterByTag === 'true') { + isTagFilter = true; + } - const sql = { - text: ` - SELECT region2_code as region_code, region2_name as region_name, region1_code as continent_code, region1_name as continent_name - FROM geohub.country - ${ - isTagFilter - ? `WHERE EXISTS (select id FROM geohub.tag WHERE key='region' and value=region2_name)` - : '' - } - ${continent_code ? `${isTagFilter ? 'AND' : 'WHERE'} region1_code = $1` : ''} - GROUP BY region2_code, region2_name, region1_code, region1_name - ORDER BY region2_name`, - values: values - }; - // console.log(sql) - const res = await client.query(sql); - return new Response(JSON.stringify(res.rows)); - } catch (err) { - return new Response(JSON.stringify({ message: err.message }), { - status: 400 - }); - } finally { - await dbm.end(); + const query = sql` + SELECT + ${countryInGeohub.region2Code} as region_code, + ${countryInGeohub.region2Name} as region_name, + ${countryInGeohub.region1Code} as continent_code, + ${countryInGeohub.region1Name} as continent_name + FROM ${countryInGeohub}`; + + if (isTagFilter) { + query.append( + sql`WHERE EXISTS ( + SELECT ${tagInGeohub.id} + FROM ${tagInGeohub} + WHERE ${tagInGeohub.key}='region' + AND ${tagInGeohub.value}=${countryInGeohub.region2Name} + )` + ); } + if (continent_code) { + if (isTagFilter) { + query.append(sql`AND`); + } else { + query.append(sql`WHERE`); + } + query.append(sql`${countryInGeohub.region1Code} = ${continent_code}`); + } + + query.append(sql` + GROUP BY ${countryInGeohub.region2Code}, ${countryInGeohub.region2Name}, ${countryInGeohub.region1Code}, ${countryInGeohub.region1Name} + ORDER BY ${countryInGeohub.region2Name} + `); + + const regions = await db.execute(query); + + return new Response(JSON.stringify(regions)); }; diff --git a/sites/geohub/src/routes/api/settings/+server.ts b/sites/geohub/src/routes/api/settings/+server.ts index ebfc4bdc7..17b4c4eb9 100644 --- a/sites/geohub/src/routes/api/settings/+server.ts +++ b/sites/geohub/src/routes/api/settings/+server.ts @@ -1,6 +1,8 @@ import type { RequestHandler } from '@sveltejs/kit'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { DefaultUserConfig, type UserConfig } from '$lib/config/DefaultUserConfig'; +import { eq } from 'drizzle-orm'; +import { userSettingsInGeohub } from '$lib/server/schema'; +import { db } from '$lib/server/db'; export const POST: RequestHandler = async ({ request, locals }) => { const session = await locals.auth(); @@ -9,25 +11,16 @@ export const POST: RequestHandler = async ({ request, locals }) => { status: 403 }); } - // Create a new DatabaseManager instance and create a new client - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const body = await request.json(); - const query = { - text: `INSERT INTO geohub.user_settings (user_email, settings) VALUES ($1, $2) ON CONFLICT (user_email) DO UPDATE SET settings = $2`, - values: [session.user.email, body] - }; - await client.query(query); - return new Response(JSON.stringify({ message: 'Settings saved' }), {}); - } catch (err) { - return new Response(JSON.stringify({ message: err.message }), { - status: 400 - }); - } finally { - await dbm.end(); - } + const user_email = session.user.email; + const settings: JSON = await request.json(); + + await db + .insert(userSettingsInGeohub) + .values({ userEmail: user_email, settings: settings }) + .onConflictDoUpdate({ target: userSettingsInGeohub.userEmail, set: { settings: settings } }); + + return new Response(JSON.stringify({ message: 'Settings saved' }), {}); }; export const GET: RequestHandler = async ({ locals }) => { @@ -35,36 +28,21 @@ export const GET: RequestHandler = async ({ locals }) => { if (!session) { return new Response(JSON.stringify(DefaultUserConfig), {}); } - const user_email = session.user.email; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: `SELECT settings FROM geohub.user_settings WHERE user_email = '${user_email}'` - }; - const res = await client.query(query); - if (res.rowCount === 0) { - // no settings found for this user in the database - return new Response(JSON.stringify(DefaultUserConfig), {}); - } - const data: UserConfig = Object.assign(DefaultUserConfig, res.rows[0].settings as UserConfig); + const user_email = session.user?.email; + const settings = await db.query.userSettingsInGeohub.findFirst({ + where: eq(userSettingsInGeohub.userEmail, user_email) + }); + if (!settings) { + // no settings found for this user in the database + return new Response(JSON.stringify(DefaultUserConfig), {}); + } else { + const data: UserConfig = Object.assign(DefaultUserConfig, settings.settings as UserConfig); if (typeof data.DataPageIngestingJoinVectorTiles === 'string') { data.DataPageIngestingJoinVectorTiles = data.DataPageIngestingJoinVectorTiles === 'true' ? true : false; } return new Response(JSON.stringify(data), {}); - } catch (err) { - return new Response( - JSON.stringify({ - message: err.message - }), - { - status: 400 - } - ); - } finally { - await dbm.end(); } }; diff --git a/sites/geohub/src/routes/api/stac/+server.ts b/sites/geohub/src/routes/api/stac/+server.ts index 1ed4af644..fd447fce3 100644 --- a/sites/geohub/src/routes/api/stac/+server.ts +++ b/sites/geohub/src/routes/api/stac/+server.ts @@ -17,9 +17,9 @@ export const GET: RequestHandler = async ({ locals, url }) => { } const type = url.searchParams.get('type'); - const stacs = await getSTACs(type); + const stacs = await getSTACs(); - return new Response(JSON.stringify(stacs)); + return new Response(JSON.stringify(stacs.filter((s) => s.type === type))); }; export const POST: RequestHandler = async ({ locals, request }) => { diff --git a/sites/geohub/src/routes/api/stac/[id]/[collection]/[...item]/[asset]/+server.ts b/sites/geohub/src/routes/api/stac/[id]/[collection]/[...item]/[asset]/+server.ts index fe3e99b90..c1569ce95 100644 --- a/sites/geohub/src/routes/api/stac/[id]/[collection]/[...item]/[asset]/+server.ts +++ b/sites/geohub/src/routes/api/stac/[id]/[collection]/[...item]/[asset]/+server.ts @@ -14,10 +14,16 @@ import type { DatasetFeature } from '$lib/types'; export const GET: RequestHandler = async ({ params, url }) => { const id = params.id; - const stacs = await getSTACs('api'); + const stacs = await getSTACs(); const stac = stacs.find((x) => x.id === id); if (!stac) { - error(400, `Only supported the following stac: ${stacs.map((x) => x.id).join(', ')}`); + error( + 400, + `Only supported the following stac: ${stacs + .filter((s) => s.type === 'api') + .map((x) => x.id) + .join(', ')}` + ); } const collection = params.collection; const asset = params.asset; diff --git a/sites/geohub/src/routes/api/stac/[id]/[collection]/[...item]/products/+server.ts b/sites/geohub/src/routes/api/stac/[id]/[collection]/[...item]/products/+server.ts index c8ecc406c..087e613de 100644 --- a/sites/geohub/src/routes/api/stac/[id]/[collection]/[...item]/products/+server.ts +++ b/sites/geohub/src/routes/api/stac/[id]/[collection]/[...item]/products/+server.ts @@ -20,10 +20,16 @@ export const POST: RequestHandler = async ({ params, url, request }) => { expression: requestBody.expression, description: requestBody.description }; - const stacs = await getSTACs('api'); + const stacs = await getSTACs(); const stac = stacs.find((x) => x.id === type); if (!stac) { - error(400, `Only supported the following stac: ${stacs.map((x) => x.id).join(', ')}`); + error( + 400, + `Only supported the following stac: ${stacs + .filter((s) => s.type === 'api') + .map((x) => x.id) + .join(', ')}` + ); } const collection = params.collection; const items = params.item.split('/'); diff --git a/sites/geohub/src/routes/api/stac/[id]/[collection]/products/+server.ts b/sites/geohub/src/routes/api/stac/[id]/[collection]/products/+server.ts index 441e9cc6b..a15cbd9dd 100644 --- a/sites/geohub/src/routes/api/stac/[id]/[collection]/products/+server.ts +++ b/sites/geohub/src/routes/api/stac/[id]/[collection]/products/+server.ts @@ -1,29 +1,26 @@ import { error, type RequestHandler } from '@sveltejs/kit'; -import DatabaseManager from '$lib/server/DatabaseManager'; +import { db } from '$lib/server/db'; +import { sql } from 'drizzle-orm'; +import { stacCollectionProductInGeohub } from '$lib/server/schema'; export const GET: RequestHandler = async ({ locals, params }) => { const session = await locals.auth(); if (!session) { error(403, { message: 'Permission error' }); } - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: `SELECT stac_id, collection_id, product_id, assets, description - FROM geohub.stac_collection_product - WHERE stac_id=$1 - AND collection_id=$2`, - values: [params.id, params.collection] - }; - const res = await client.query(query); - const products = res.rows; - return new Response(JSON.stringify(products)); - } catch (err) { - await dbm.transactionRollback(); - error(500, err); - } finally { - await dbm.end(); - } + const products = await db + .select({ + stac_id: stacCollectionProductInGeohub.stacId, + collection_id: stacCollectionProductInGeohub.collectionId, + product_id: stacCollectionProductInGeohub.productId, + assets: stacCollectionProductInGeohub.assets, + description: stacCollectionProductInGeohub.description + }) + .from(stacCollectionProductInGeohub).where(sql` + ${stacCollectionProductInGeohub.stacId} = ${params.id} + AND + ${stacCollectionProductInGeohub.collectionId} = ${params.collection} + `); + return new Response(JSON.stringify(products)); }; diff --git a/sites/geohub/src/routes/api/stac/[id]/[collection]/products/[product_id]/+server.ts b/sites/geohub/src/routes/api/stac/[id]/[collection]/products/[product_id]/+server.ts index 1a3bf22fe..fc9b63dff 100644 --- a/sites/geohub/src/routes/api/stac/[id]/[collection]/products/[product_id]/+server.ts +++ b/sites/geohub/src/routes/api/stac/[id]/[collection]/products/[product_id]/+server.ts @@ -1,6 +1,8 @@ import { error, type RequestHandler } from '@sveltejs/kit'; -import { isSuperuser, getProductDetails, deleteProduct } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; +import { isSuperuser } from '$lib/server/helpers'; +import { db } from '$lib/server/db'; +import { productInGeohub, stacCollectionProductInGeohub } from '$lib/server/schema'; +import { eq, sql } from 'drizzle-orm'; export const GET: RequestHandler = async ({ locals, params }) => { // get product details @@ -13,24 +15,41 @@ export const GET: RequestHandler = async ({ locals, params }) => { error(403, { message: 'Permission error' }); } - const productDetails = await getProductDetails(params.id, params.collection, params.product_id); - if (Object.keys(productDetails).length === 0) { + const productDetails = await db + .select({ + stac_id: stacCollectionProductInGeohub.stacId, + collection_id: stacCollectionProductInGeohub.collectionId, + product_id: stacCollectionProductInGeohub.productId, + assets: stacCollectionProductInGeohub.assets, + label: productInGeohub.label, + expression: productInGeohub.expression, + description: productInGeohub.description + }) + .from(stacCollectionProductInGeohub) + .innerJoin(productInGeohub, eq(stacCollectionProductInGeohub.productId, productInGeohub.id)) + .where(sql` + ${stacCollectionProductInGeohub.stacId}=${params.id} + AND ${stacCollectionProductInGeohub.collectionId}=${params.collection} + AND ${stacCollectionProductInGeohub.productId}=${params.product_id} + `); + + if (productDetails.length === 0) { error(404, { message: 'Not found' }); } - const expression = productDetails?.expression; - const assets = productDetails?.assets; + + const detail = productDetails[0]; + + const expression = detail.expression; + const assets = detail.assets; const asset_index_mapping = assets.map((asset: string, index: number) => { return { [`asset${index + 1}`]: asset }; }); - productDetails.expression = replaceTextWithMapping(expression, asset_index_mapping); + detail.expression = replaceTextWithMapping(expression, asset_index_mapping); - if (!productDetails) { - error(404, { message: 'Not found' }); - } - return new Response(JSON.stringify(productDetails)); + return new Response(JSON.stringify(detail)); }; export const POST: RequestHandler = async ({ locals, params, request }) => { @@ -57,27 +76,23 @@ export const POST: RequestHandler = async ({ locals, params, request }) => { const requestBody = await request.json(); const assets = requestBody.assets; const description = requestBody.description; - const stac_id = params.id; - const collection_id = params.collection; - const product_id = params.product_id; - - const dbm = new DatabaseManager(); - const client = await dbm.start(); - - const query = { - text: `INSERT INTO geohub.stac_collection_product (stac_id, collection_id, product_id, assets, description) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (stac_id, collection_id, product_id) DO NOTHING;`, - values: [stac_id, collection_id, product_id, assets, description] - }; - - try { - await client.query(query); - return new Response(JSON.stringify({ message: 'Product registered' })); - } catch (err) { - await dbm.transactionRollback(); - error(500, err); - } finally { - await dbm.transactionEnd(); - } + const stac_id = params.id as string; + const collection_id = params.collection as string; + const product_id = params.product_id as string; + + const result = await db + .insert(stacCollectionProductInGeohub) + .values({ + stacId: stac_id, + collectionId: collection_id, + productId: product_id, + assets: assets, + description: description + }) + .onConflictDoNothing() + .returning(); + + return new Response(JSON.stringify(result)); }; export const DELETE: RequestHandler = async ({ locals, params }) => { @@ -98,16 +113,36 @@ export const DELETE: RequestHandler = async ({ locals, params }) => { if (!is_superuser) { error(403, { message: 'Permission error' }); } - const stacId = params.id; - const collectionId = params.collection; - const productId = params.product_id; - const deleteResult = await deleteProduct(stacId, collectionId, productId); - return new Response(JSON.stringify(deleteResult)); + const res = await db + .delete(stacCollectionProductInGeohub) + .where( + sql` + ${stacCollectionProductInGeohub.stacId}=${params.id} + AND ${stacCollectionProductInGeohub.collectionId}=${params.collection} + AND ${stacCollectionProductInGeohub.productId}=${params.product_id} + ` + ) + .returning(); + if (res.length === 0) { + error(404, { + message: `${[params.id, params.collection, params.product_id].join(', ')} does not exist in the database` + }); + } + return new Response( + JSON.stringify({ + message: 'Product deleted' + }) + ); }; // Function to replace text based on mapping -const replaceTextWithMapping = (text: string, mapping: never[]) => { +const replaceTextWithMapping = ( + text: string, + mapping: { + [x: string]: string; + }[] +) => { for (let i = 0; i < mapping.length; i++) { const key = Object.keys(mapping[i])[0]; const value = mapping[i][key]; diff --git a/sites/geohub/src/routes/api/stac/catalog/[id]/item/+server.ts b/sites/geohub/src/routes/api/stac/catalog/[id]/item/+server.ts index 1f1e873e4..31e957b3f 100644 --- a/sites/geohub/src/routes/api/stac/catalog/[id]/item/+server.ts +++ b/sites/geohub/src/routes/api/stac/catalog/[id]/item/+server.ts @@ -13,7 +13,8 @@ import { generateHashKey, resolveRelativeUrl } from '$lib/helper'; export const GET: RequestHandler = async ({ params, url }) => { const id = params.id; - const stacCatalogs = await getSTACs('catalog'); + const stacs = await getSTACs(); + const stacCatalogs = stacs.filter((s) => s.type === 'catalog'); const stac = stacCatalogs.find((x) => x.id === id); if (!stac) { error(400, `Only supported the following stac: ${stacCatalogs.map((x) => x.id).join(', ')}`); diff --git a/sites/geohub/src/routes/api/storymaps/+server.ts b/sites/geohub/src/routes/api/storymaps/+server.ts index c10975a4c..91631b3db 100644 --- a/sites/geohub/src/routes/api/storymaps/+server.ts +++ b/sites/geohub/src/routes/api/storymaps/+server.ts @@ -1,6 +1,5 @@ import type { RequestHandler } from './$types'; import type { StoryMapConfig, StoryMapChapter, Pages, Link } from '$lib/types'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { error } from '@sveltejs/kit'; import StorymapManager from '$lib/server/StorymapManager'; import { isSuperuser, pageNumber } from '$lib/server/helpers'; @@ -8,53 +7,66 @@ import { Permission } from '$lib/config/AppConfig'; export const GET: RequestHandler = async ({ url, locals }) => { const session = await locals.auth(); - const user_email = session?.user.email; + const user_email = session?.user.email as string; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const _limit = url.searchParams.get('limit') || 10; - const limit = Number(_limit); - const _offset = url.searchParams.get('offset') || 0; - const offset = Number(_offset); - - let is_superuser = false; - if (user_email) { - is_superuser = await isSuperuser(user_email); - } + const _limit = url.searchParams.get('limit') || 10; + const limit = Number(_limit); + const _offset = url.searchParams.get('offset') || 0; + const offset = Number(_offset); + + let is_superuser = false; + if (user_email) { + is_superuser = await isSuperuser(user_email); + } - let query = url.searchParams.get('query'); + let query = url.searchParams.get('query') as string; - const values = []; - if (query) { - // normalise query text for to_tsquery function - query = query - .toLowerCase() - .replace(/\r?\s+and\s+/g, ' & ') // convert 'and' to '&' - .replace(/\r?\s+or\s+/g, ' | '); // convert 'or' to '|' - values.push(query); - } + const values = []; + if (query) { + // normalise query text for to_tsquery function + query = query + .toLowerCase() + .replace(/\r?\s+and\s+/g, ' & ') // convert 'and' to '&' + .replace(/\r?\s+or\s+/g, ' | '); // convert 'or' to '|' + values.push(query); + } - const accessLevel = Number(url.searchParams.get('accesslevel') ?? '1'); + const accessLevel = Number(url.searchParams.get('accesslevel') ?? '1'); - const _onlyStar = url.searchParams.get('staronly') || 'false'; - const onlyStar = _onlyStar.toLowerCase() === 'true'; + const _onlyStar = url.searchParams.get('staronly') || 'false'; + const onlyStar = _onlyStar.toLowerCase() === 'true'; - const _onlyMydata = url.searchParams.get('mydata') || 'false'; - const mydataOnly = _onlyMydata.toLowerCase() === 'true'; + const _onlyMydata = url.searchParams.get('mydata') || 'false'; + const mydataOnly = _onlyMydata.toLowerCase() === 'true'; + + const sortby = url.searchParams.get('sortby'); + let sortByColumn = 'updatedat'; + let sortOrder: 'asc' | 'desc' = 'desc'; + if (sortby) { + const values = sortby.split(','); + const column: string = values[0].trim().toLowerCase(); + const targetSortingColumns = ['title', 'createdat', 'updatedat', 'no_stars']; + const targetSortingOrder = ['asc', 'desc']; + if (!targetSortingColumns.includes(column)) { + return new Response( + JSON.stringify({ + message: `Bad parameter for 'sortby'. It must be one of '${targetSortingColumns.join( + ', ' + )}'` + }), + { + status: 400 + } + ); + } + sortByColumn = column; - const sortby = url.searchParams.get('sortby'); - let sortByColumn = 'updatedat'; - let sortOrder: 'asc' | 'desc' = 'desc'; - if (sortby) { - const values = sortby.split(','); - const column: string = values[0].trim().toLowerCase(); - const targetSortingColumns = ['title', 'createdat', 'updatedat', 'no_stars']; - const targetSortingOrder = ['asc', 'desc']; - if (!targetSortingColumns.includes(column)) { + if (values.length > 1) { + const order: string = values[1].trim().toLowerCase(); + if (!targetSortingOrder.includes(order)) { return new Response( JSON.stringify({ - message: `Bad parameter for 'sortby'. It must be one of '${targetSortingColumns.join( + message: `Bad parameter for 'sortby'. Sorting order must be one of '${targetSortingOrder.join( ', ' )}'` }), @@ -63,117 +75,94 @@ export const GET: RequestHandler = async ({ url, locals }) => { } ); } - sortByColumn = column; - - if (values.length > 1) { - const order: string = values[1].trim().toLowerCase(); - if (!targetSortingOrder.includes(order)) { - return new Response( - JSON.stringify({ - message: `Bad parameter for 'sortby'. Sorting order must be one of '${targetSortingOrder.join( - ', ' - )}'` - }), - { - status: 400 - } - ); - } - sortOrder = order as 'asc' | 'desc'; - } + sortOrder = order as 'asc' | 'desc'; } + } - const sm = new StorymapManager(); - const stories = await sm.search( - client, - query, - limit, - offset, - accessLevel, - onlyStar, - sortByColumn, - sortOrder, - is_superuser, - user_email, - mydataOnly - ); - - const nextUrl = new URL(url.toString()); - nextUrl.searchParams.set('limit', limit.toString()); - nextUrl.searchParams.set('offset', (Number(offset) + Number(limit)).toString()); - - const links: Link[] = [ - { - rel: 'root', - type: 'application/json', - href: `${url.origin}${url.pathname}` - }, - { - rel: 'self', - type: 'application/json', - href: url.toString() - } - ]; - - if (stories.length === Number(limit)) { - links.push({ - rel: 'next', - type: 'application/json', - href: nextUrl.toString() - }); + const sm = new StorymapManager(); + const stories = await sm.search( + query, + limit, + offset, + accessLevel, + onlyStar, + sortByColumn, + sortOrder, + is_superuser, + user_email, + mydataOnly + ); + + const nextUrl = new URL(url.toString()); + nextUrl.searchParams.set('limit', limit.toString()); + nextUrl.searchParams.set('offset', (Number(offset) + Number(limit)).toString()); + + const links: Link[] = [ + { + rel: 'root', + type: 'application/json', + href: `${url.origin}${url.pathname}` + }, + { + rel: 'self', + type: 'application/json', + href: url.toString() } + ]; - if (Number(offset) > 0) { - const previoustUrl = new URL(url.toString()); - previoustUrl.searchParams.set('limit', limit.toString()); - previoustUrl.searchParams.set('offset', (Number(offset) - Number(limit)).toString()); + if (stories.length === Number(limit)) { + links.push({ + rel: 'next', + type: 'application/json', + href: nextUrl.toString() + }); + } - links.push({ - rel: 'previous', - type: 'application/json', - href: previoustUrl.toString() - }); - } + if (Number(offset) > 0) { + const previoustUrl = new URL(url.toString()); + previoustUrl.searchParams.set('limit', limit.toString()); + previoustUrl.searchParams.set('offset', (Number(offset) - Number(limit)).toString()); - stories.forEach((story) => { - story.links = story.links.map((l) => { - const _url = new URL(l.href, url.origin); - const subUrl = _url.searchParams.get('url'); - if (subUrl) { - _url.searchParams.set('url', new URL(subUrl, url.origin).href); - } - l.href = decodeURI(_url.href); - return l; - }); + links.push({ + rel: 'previous', + type: 'application/json', + href: previoustUrl.toString() }); + } - const totalCount = await sm.getTotalCount( - client, - query, - accessLevel, - onlyStar, - is_superuser, - user_email, - mydataOnly - ); - let totalPages = Math.ceil(totalCount / Number(limit)); - if (totalPages === 0) { - totalPages = 1; - } + stories.forEach((story) => { + story.links = story.links.map((l) => { + const _url = new URL(l.href, url.origin); + const subUrl = _url.searchParams.get('url'); + if (subUrl) { + _url.searchParams.set('url', new URL(subUrl, url.origin).href); + } + l.href = decodeURI(_url.href); + return l; + }); + }); - const currentPage = pageNumber(totalCount, Number(limit), Number(offset)); - const pages: Pages = { - totalCount, - totalPages, - currentPage - }; - - return new Response(JSON.stringify({ stories, links, pages })); - } catch (err) { - error(500, err); - } finally { - dbm.end(); + const totalCount = await sm.getTotalCount( + query, + accessLevel, + onlyStar, + is_superuser, + user_email, + mydataOnly + ); + let totalPages = Math.ceil(totalCount / Number(limit)); + if (totalPages === 0) { + totalPages = 1; } + + const currentPage = pageNumber(totalCount, Number(limit), Number(offset)); + const pages: Pages = { + totalCount, + totalPages, + currentPage + }; + + return new Response(JSON.stringify({ stories, links, pages })); }; export const POST: RequestHandler = async ({ locals, request }) => { @@ -204,31 +193,23 @@ export const POST: RequestHandler = async ({ locals, request }) => { chapter.updatedat = body.updatedat; }); - const dbm = new DatabaseManager(); - let storymap: StoryMapConfig; - try { - const client = await dbm.transactionStart(); + const sm = new StorymapManager(body); - const sm = new StorymapManager(body); - - const story = await sm.getById(client, body.id, is_superuser, user_email); - // if story id already exists, check user permission - if (story && !(story && story.permission >= Permission.WRITE)) { - return new Response( - JSON.stringify({ message: `You don't have permission to edit this storymap.` }), - { - status: 403 - } - ); - } + const story = await sm.getById(body.id as string, is_superuser, user_email); + // if story id already exists, check user permission + if (story && !(story && story.permission >= Permission.WRITE)) { + return new Response( + JSON.stringify({ message: `You don't have permission to edit this storymap.` }), + { + status: 403 + } + ); + } - storymap = await sm.upsert(client); - storymap = await sm.getById(client, storymap.id, is_superuser, user_email); - } catch (err) { - dbm.transactionRollback(); - error(500, err); - } finally { - await dbm.transactionEnd(); + let storymap = await sm.upsert(); + if (storymap) { + storymap = await sm.getById(storymap.id as string, is_superuser, user_email); } + return new Response(JSON.stringify(storymap)); }; diff --git a/sites/geohub/src/routes/api/storymaps/[id]/+server.ts b/sites/geohub/src/routes/api/storymaps/[id]/+server.ts index 3641dc9c9..7aab419f6 100644 --- a/sites/geohub/src/routes/api/storymaps/[id]/+server.ts +++ b/sites/geohub/src/routes/api/storymaps/[id]/+server.ts @@ -1,15 +1,13 @@ import type { RequestHandler } from './$types'; import { isSuperuser } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { AccessLevel, Permission } from '$lib/config/AppConfig'; import StorymapManager from '$lib/server/StorymapManager'; import { StorymapPermissionManager } from '$lib/server/StorymapPermissionManager'; import { getDomainFromEmail } from '$lib/helper'; -import { error } from '@sveltejs/kit'; export const GET: RequestHandler = async ({ params, locals, url }) => { const session = await locals.auth(); - const user_email = session?.user.email; + const user_email = session?.user.email as string; let is_superuser = false; if (user_email) { is_superuser = await isSuperuser(user_email); @@ -17,68 +15,60 @@ export const GET: RequestHandler = async ({ params, locals, url }) => { const id = params.id; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const sm = new StorymapManager(); - const story = await sm.getById(client, id, is_superuser, user_email); - if (!story) { - return new Response(JSON.stringify({ message: `No storymap found.` }), { - status: 404 - }); - } - - if (story.style) { - story.style = `${url.origin}${story.style}`; - } - story.chapters.forEach((ch) => { - ch.style = `${url.origin}${ch.style}`; + const sm = new StorymapManager(); + const story = await sm.getById(id, is_superuser, user_email); + if (!story) { + return new Response(JSON.stringify({ message: `No storymap found.` }), { + status: 404 }); + } - story.links = story.links.map((l) => { - const _url = new URL(decodeURI(l.href), url.origin); - const subUrl = _url.searchParams.get('url'); - if (subUrl) { - _url.searchParams.set('url', new URL(subUrl, url.origin).href); - } - l.href = decodeURI(_url.href); - return l; - }); + if (story.style) { + story.style = `${url.origin}${story.style}`; + } + story.chapters.forEach((ch) => { + ch.style = `${url.origin}${ch.style}`; + }); - if (!is_superuser) { - const sp = new StorymapPermissionManager(id, user_email); - const permission = await sp.getBySignedUser(client); - if (!(permission && permission >= Permission.READ)) { - const domain = user_email ? getDomainFromEmail(user_email) : undefined; - const access_level: AccessLevel = story.access_level; - if (access_level === AccessLevel.PRIVATE) { - if (story.created_user !== user_email) { - return new Response( - JSON.stringify({ message: `No permission to access to this storymap.` }), - { - status: 403 - } - ); - } - } else if (access_level === AccessLevel.ORGANIZATION) { - if (!story.created_user.endsWith(domain)) { - return new Response( - JSON.stringify({ message: `No permission to access to this storymap.` }), - { - status: 403 - } - ); - } + story.links = story.links.map((l) => { + const _url = new URL(decodeURI(l.href), url.origin); + const subUrl = _url.searchParams.get('url'); + if (subUrl) { + _url.searchParams.set('url', new URL(subUrl, url.origin).href); + } + l.href = decodeURI(_url.href); + return l; + }); + + if (!is_superuser) { + const sp = new StorymapPermissionManager(id, user_email); + const permission = await sp.getBySignedUser(); + if (!(permission && permission >= Permission.READ)) { + const domain = user_email ? getDomainFromEmail(user_email) : undefined; + const access_level: AccessLevel = story.access_level; + if (access_level === AccessLevel.PRIVATE) { + if (story.created_user !== user_email) { + return new Response( + JSON.stringify({ message: `No permission to access to this storymap.` }), + { + status: 403 + } + ); + } + } else if (access_level === AccessLevel.ORGANIZATION) { + if (!story.created_user.endsWith(domain)) { + return new Response( + JSON.stringify({ message: `No permission to access to this storymap.` }), + { + status: 403 + } + ); } } } - - return new Response(JSON.stringify(story)); - } catch (err) { - error(500, err); - } finally { - dbm.end(); } + + return new Response(JSON.stringify(story)); }; export const DELETE: RequestHandler = async ({ params, locals }) => { @@ -96,37 +86,28 @@ export const DELETE: RequestHandler = async ({ params, locals }) => { const id = params.id; - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - try { - let sm = new StorymapManager(); + let sm = new StorymapManager(); - const story = await sm.getById(client, id, is_superuser, user_email); - if (!story) { - return new Response(JSON.stringify({ message: `No storymap found.` }), { - status: 404 - }); - } + const story = await sm.getById(id, is_superuser, user_email); + if (!story) { + return new Response(JSON.stringify({ message: `No storymap found.` }), { + status: 404 + }); + } - if (!(story.permission === Permission.OWNER)) { - return new Response( - JSON.stringify({ message: `You don't have permission to delete this storymap.` }), - { - status: 403 - } - ); - } + if (!(story.permission === Permission.OWNER)) { + return new Response( + JSON.stringify({ message: `You don't have permission to delete this storymap.` }), + { + status: 403 + } + ); + } - sm = new StorymapManager(story); - await sm.delete(client, story.id); + sm = new StorymapManager(story); + await sm.delete(story.id as string); - return new Response(undefined, { - status: 204 - }); - } catch (err) { - await dbm.transactionRollback(); - error(500, err); - } finally { - await dbm.transactionEnd(); - } + return new Response(undefined, { + status: 204 + }); }; diff --git a/sites/geohub/src/routes/api/storymaps/[id]/permission/+server.ts b/sites/geohub/src/routes/api/storymaps/[id]/permission/+server.ts index b232bae65..8fdbf06c2 100644 --- a/sites/geohub/src/routes/api/storymaps/[id]/permission/+server.ts +++ b/sites/geohub/src/routes/api/storymaps/[id]/permission/+server.ts @@ -1,22 +1,19 @@ import type { RequestHandler } from './$types'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { error } from '@sveltejs/kit'; import { StorymapPermissionManager, type StorymapPermission } from '$lib/server/StorymapPermissionManager.ts'; -import type { PoolClient } from 'pg'; - -const storymapExists = async (client: PoolClient, id: string) => { - const query = { - text: `SELECT id FROM geohub.storymap WHERE id= $1`, - values: [id] - }; - const res = await client.query(query); - if (res.rowCount === 0) { - return false; - } - return true; +import { db } from '$lib/server/db'; +import { eq } from 'drizzle-orm'; +import { storymapInGeohub } from '$lib/server/schema'; + +const storymapExists = async (id: string) => { + const st = await db + .select({ id: storymapInGeohub.id }) + .from(storymapInGeohub) + .where(eq(storymapInGeohub.id, id)); + return st && st.length > 0; }; export const GET: RequestHandler = async ({ params, locals }) => { @@ -27,23 +24,15 @@ export const GET: RequestHandler = async ({ params, locals }) => { const id = params.id; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const exists = await storymapExists(client, id); - if (!exists) { - error(404, { message: `No storymap found.` }); - } - - const dpm = new StorymapPermissionManager(id, user_email); - const permissions = await dpm.getAll(client); - - return new Response(JSON.stringify(permissions)); - } catch (err) { - error(500, err); - } finally { - dbm.end(); + const exists = await storymapExists(id); + if (!exists) { + error(404, { message: `No storymap found.` }); } + + const dpm = new StorymapPermissionManager(id, user_email); + const permissions = await dpm.getAll(); + + return new Response(JSON.stringify(permissions)); }; export const POST: RequestHandler = async ({ params, locals, request }) => { @@ -74,25 +63,16 @@ export const POST: RequestHandler = async ({ params, locals, request }) => { permission: body.permission }; - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - - try { - const exists = await storymapExists(client, id); - if (!exists) { - error(404, { message: `No storymap found.` }); - } + const exists = await storymapExists(id); + if (!exists) { + error(404, { message: `No storymap found.` }); + } - const dpm = new StorymapPermissionManager(id, user_email); - await dpm.register(client, permission); - const permissions = await dpm.getAll(client); + const dpm = new StorymapPermissionManager(id, user_email); + await dpm.register(permission); + const permissions = await dpm.getAll(); - return new Response(JSON.stringify(permissions)); - } catch (err) { - error(500, err); - } finally { - dbm.transactionEnd(); - } + return new Response(JSON.stringify(permissions)); }; export const PUT: RequestHandler = async ({ params, locals, request }) => { @@ -127,24 +107,16 @@ export const PUT: RequestHandler = async ({ params, locals, request }) => { createdat: body.createdat }; - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - try { - const exists = await storymapExists(client, id); - if (!exists) { - error(404, { message: `No storymap found.` }); - } - - const dpm = new StorymapPermissionManager(id, user_email); - await dpm.update(client, permission); - const permissions = await dpm.getAll(client); - - return new Response(JSON.stringify(permissions)); - } catch (err) { - error(500, err); - } finally { - dbm.transactionEnd(); + const exists = await storymapExists(id); + if (!exists) { + error(404, { message: `No storymap found.` }); } + + const dpm = new StorymapPermissionManager(id, user_email); + await dpm.update(permission); + const permissions = await dpm.getAll(); + + return new Response(JSON.stringify(permissions)); }; export const DELETE: RequestHandler = async ({ params, locals, url }) => { @@ -159,22 +131,14 @@ export const DELETE: RequestHandler = async ({ params, locals, url }) => { error(400, { message: `query parameter of user_email is required.` }); } - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - try { - const exists = await storymapExists(client, id); - if (!exists) { - error(404, { message: `No storymap found.` }); - } - - const dpm = new StorymapPermissionManager(id, user_email); - await dpm.delete(client, target_email); - const permissions = await dpm.getAll(client); - - return new Response(JSON.stringify(permissions)); - } catch (err) { - error(500, err); - } finally { - dbm.transactionEnd(); + const exists = await storymapExists(id); + if (!exists) { + error(404, { message: `No storymap found.` }); } + + const dpm = new StorymapPermissionManager(id, user_email); + await dpm.delete(target_email); + const permissions = await dpm.getAll(); + + return new Response(JSON.stringify(permissions)); }; diff --git a/sites/geohub/src/routes/api/storymaps/[id]/star/+server.ts b/sites/geohub/src/routes/api/storymaps/[id]/star/+server.ts index 40e8855c7..abb338c33 100644 --- a/sites/geohub/src/routes/api/storymaps/[id]/star/+server.ts +++ b/sites/geohub/src/routes/api/storymaps/[id]/star/+server.ts @@ -1,7 +1,9 @@ import type { RequestHandler } from './$types'; import { getStoryStarCount } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { error } from '@sveltejs/kit'; +import { storymapFavouriteInGeohub } from '$lib/server/schema'; +import { db } from '$lib/server/db'; +import { sql } from 'drizzle-orm'; export const POST: RequestHandler = async ({ locals, params }) => { const session = await locals.auth(); @@ -13,44 +15,26 @@ export const POST: RequestHandler = async ({ locals, params }) => { const user_email = session.user.email; const now = new Date().toISOString(); - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: ` - INSERT INTO geohub.storymap_favourite ( - storymap_id, user_email, savedat - ) values ( - $1, - $2, - $3::timestamptz - ) - ON CONFLICT (storymap_id, user_email) - DO - UPDATE - SET - savedat=$3::timestamptz - `, - values: [storymap_id, user_email, now] - }; - - await client.query(query); - - const stars = await getStoryStarCount(client, storymap_id); - - const res = { - storymap_id, - user_email, - savedat: now, - no_stars: stars - }; - - return new Response(JSON.stringify(res)); - } catch (err) { - error(500, err); - } finally { - dbm.end(); - } + await db + .insert(storymapFavouriteInGeohub) + .values({ storymapId: storymap_id, userEmail: user_email, savedat: now }) + .onConflictDoUpdate({ + target: [storymapFavouriteInGeohub.storymapId, storymapFavouriteInGeohub.userEmail], + set: { + savedat: now + } + }); + + const stars = await getStoryStarCount(storymap_id); + + const res = { + storymap_id, + user_email, + savedat: now, + no_stars: stars + }; + + return new Response(JSON.stringify(res)); }; export const DELETE: RequestHandler = async ({ locals, params }) => { @@ -62,27 +46,18 @@ export const DELETE: RequestHandler = async ({ locals, params }) => { const storymap_id = params.id; const user_email = session.user.email; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: `DELETE FROM geohub.storymap_favourite WHERE storymap_id=$1 and user_email=$2`, - values: [storymap_id, user_email] - }; - - await client.query(query); + await db + .delete(storymapFavouriteInGeohub) + .where( + sql`${storymapFavouriteInGeohub.storymapId} = ${storymap_id} AND ${storymapFavouriteInGeohub.userEmail} = ${user_email}` + ); - const stars = await getStoryStarCount(client, storymap_id); + const stars = await getStoryStarCount(storymap_id); - const res = { - storymap_id, - no_stars: stars - }; + const res = { + storymap_id, + no_stars: stars + }; - return new Response(JSON.stringify(res)); - } catch (err) { - error(400, err); - } finally { - dbm.end(); - } + return new Response(JSON.stringify(res)); }; diff --git a/sites/geohub/src/routes/api/storymaps/[id]/star/count/+server.ts b/sites/geohub/src/routes/api/storymaps/[id]/star/count/+server.ts index de6c9d139..e67cbe41e 100644 --- a/sites/geohub/src/routes/api/storymaps/[id]/star/count/+server.ts +++ b/sites/geohub/src/routes/api/storymaps/[id]/star/count/+server.ts @@ -1,25 +1,15 @@ import type { RequestHandler } from './$types'; import { getStoryStarCount } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; -import { error } from '@sveltejs/kit'; export const GET: RequestHandler = async ({ params }) => { const storymap_id = params.id; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const stars = await getStoryStarCount(client, storymap_id); + const stars = await getStoryStarCount(storymap_id); - const res = { - storymap_id, - no_stars: stars - }; + const res = { + storymap_id, + no_stars: stars + }; - return new Response(JSON.stringify(res)); - } catch (err) { - error(500, err); - } finally { - dbm.end(); - } + return new Response(JSON.stringify(res)); }; diff --git a/sites/geohub/src/routes/api/style/+server.ts b/sites/geohub/src/routes/api/style/+server.ts index 8909c0532..43f6ce09d 100644 --- a/sites/geohub/src/routes/api/style/+server.ts +++ b/sites/geohub/src/routes/api/style/+server.ts @@ -8,11 +8,13 @@ import { isSuperuser } from '$lib/server/helpers'; import { AccessLevel, Permission } from '$lib/config/AppConfig'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { getDomainFromEmail } from '$lib/helper'; import { error } from '@sveltejs/kit'; import type { StyleSpecification } from 'maplibre-gl'; import { StylePermissionManager } from '$lib/server/StylePermissionManager.ts'; +import { db, type TransactionSchema } from '$lib/server/db'; +import { styleInGeohub } from '$lib/server/schema'; +import { eq, SQL, sql } from 'drizzle-orm'; /** * Get the list of saved style from PostGIS database @@ -36,25 +38,38 @@ export const GET: RequestHandler = async ({ url, locals }) => { is_superuser = await isSuperuser(user_email); } - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const limit = url.searchParams.get('limit') ?? '10'; - const offset = url.searchParams.get('offset') ?? '0'; - const accessLevel = Number(url.searchParams.get('accesslevel') ?? '1'); - - const sortby = url.searchParams.get('sortby'); - let sortByColumn = 'name'; - let sortOrder: 'asc' | 'desc' = 'asc'; - if (sortby) { - const values = sortby.split(','); - const column: string = values[0].trim().toLowerCase(); - const targetSortingColumns = ['id', 'name', 'createdat', 'updatedat', 'no_stars']; - const targetSortingOrder = ['asc', 'desc']; - if (!targetSortingColumns.includes(column)) { + const limit = url.searchParams.get('limit') ?? '10'; + const offset = url.searchParams.get('offset') ?? '0'; + const accessLevel = Number(url.searchParams.get('accesslevel') ?? '1'); + + const sortby = url.searchParams.get('sortby'); + let sortByColumn = 'name'; + let sortOrder: 'asc' | 'desc' = 'asc'; + if (sortby) { + const values = sortby.split(','); + const column: string = values[0].trim().toLowerCase(); + const targetSortingColumns = ['id', 'name', 'createdat', 'updatedat', 'no_stars']; + const targetSortingOrder = ['asc', 'desc']; + if (!targetSortingColumns.includes(column)) { + return new Response( + JSON.stringify({ + message: `Bad parameter for 'sortby'. It must be one of '${targetSortingColumns.join( + ', ' + )}'` + }), + { + status: 400 + } + ); + } + sortByColumn = column; + + if (values.length > 1) { + const order: string = values[1].trim().toLowerCase(); + if (!targetSortingOrder.includes(order)) { return new Response( JSON.stringify({ - message: `Bad parameter for 'sortby'. It must be one of '${targetSortingColumns.join( + message: `Bad parameter for 'sortby'. Sorting order must be one of '${targetSortingOrder.join( ', ' )}'` }), @@ -63,51 +78,35 @@ export const GET: RequestHandler = async ({ url, locals }) => { } ); } - sortByColumn = column; - - if (values.length > 1) { - const order: string = values[1].trim().toLowerCase(); - if (!targetSortingOrder.includes(order)) { - return new Response( - JSON.stringify({ - message: `Bad parameter for 'sortby'. Sorting order must be one of '${targetSortingOrder.join( - ', ' - )}'` - }), - { - status: 400 - } - ); - } - sortOrder = order as 'asc' | 'desc'; - } + sortOrder = order as 'asc' | 'desc'; } + } - let query = url.searchParams.get('query'); + let query = url.searchParams.get('query'); - const values = []; - if (query) { - // normalise query text for to_tsquery function - query = query - .toLowerCase() - .replace(/\r?\s+and\s+/g, ' & ') // convert 'and' to '&' - .replace(/\r?\s+or\s+/g, ' | '); // convert 'or' to '|' - values.push(query); - } + const values = []; + if (query) { + // normalise query text for to_tsquery function + query = query + .toLowerCase() + .replace(/\r?\s+and\s+/g, ' & ') // convert 'and' to '&' + .replace(/\r?\s+or\s+/g, ' | '); // convert 'or' to '|' + values.push(query); + } - const email = session?.user?.email; - let domain: string; - if (email) { - domain = getDomainFromEmail(email); - } + const email = session?.user?.email; + let domain: string; + if (email) { + domain = getDomainFromEmail(email); + } - const _onlyStar = url.searchParams.get('staronly') || 'false'; - const onlyStar = _onlyStar.toLowerCase() === 'true'; + const _onlyStar = url.searchParams.get('staronly') || 'false'; + const onlyStar = _onlyStar.toLowerCase() === 'true'; - const _onlyMydata = url.searchParams.get('mydata') || 'false'; - const mydataOnly = _onlyMydata.toLowerCase() === 'true'; + const _onlyMydata = url.searchParams.get('mydata') || 'false'; + const mydataOnly = _onlyMydata.toLowerCase() === 'true'; - const where = ` + const where = sql.raw(` WHERE ( ${accessLevel === AccessLevel.PUBLIC ? `x.access_level = ${AccessLevel.PUBLIC}` : ''} @@ -152,7 +151,7 @@ export const GET: RequestHandler = async ({ url, locals }) => { } ) - ${query ? 'AND to_tsvector(x.name) @@ to_tsquery($1)' : ''} + ${query ? `AND to_tsvector(x.name) @@ to_tsquery('${query}')` : ''} ${ onlyStar && user_email ? ` @@ -168,14 +167,13 @@ export const GET: RequestHandler = async ({ url, locals }) => { AND EXISTS (SELECT style_id FROM geohub.style_permission WHERE style_id = x.id AND user_email = '${user_email}' AND permission >= ${Permission.READ} )` : '' } - `; - - // only can access to - // access_level = 3 - // or access_lavel = 2 which were created by user with @undp.org email - // or created_user = login user email - const sql = { - text: ` + `); + + // only can access to + // access_level = 3 + // or access_lavel = 2 which were created by user with @undp.org email + // or created_user = login user email + const mainSql = sql.raw(` with no_stars as ( SELECT style_id, count(*) as no_stars FROM geohub.style_favourite GROUP BY style_id ) @@ -218,79 +216,80 @@ AND EXISTS (SELECT style_id FROM geohub.style_permission WHERE style_id = x.id A LEFT JOIN no_stars z ON x.id = z.style_id LEFT JOIN permission p - ON x.id = p.style_id - ${where} - ORDER BY - ${sortByColumn} ${sortOrder} - LIMIT ${limit} - OFFSET ${offset}`, - values: values - }; - - // console.log(sql); - - const res = await client.query(sql); - - const nextUrl = new URL(url.toString()); - nextUrl.searchParams.set('limit', limit); - nextUrl.searchParams.set('offset', (Number(offset) + Number(limit)).toString()); + ON x.id = p.style_id`); - const links: Link[] = [ - { - rel: 'root', - type: 'application/json', - href: `${url.origin}${url.pathname}` - }, - { - rel: 'self', - type: 'application/json', - href: url.toString() - } - ]; - - if (res.rowCount === Number(limit)) { - links.push({ - rel: 'next', - type: 'application/json', - href: nextUrl.toString() - }); + const sqlChunks: SQL[] = [mainSql]; + if (where) { + sqlChunks.push(where); + } + sqlChunks.push( + sql.raw(` + ORDER BY + ${sortByColumn} ${sortOrder} + LIMIT ${limit} + OFFSET ${offset} + `) + ); + const finalSql: SQL = sql.join(sqlChunks, sql.raw(' ')); + + const styles: DashboardMapStyle[] = (await db.execute( + finalSql + )) as unknown as DashboardMapStyle[]; + + const nextUrl = new URL(url.toString()); + nextUrl.searchParams.set('limit', limit); + nextUrl.searchParams.set('offset', (Number(offset) + Number(limit)).toString()); + + const links: Link[] = [ + { + rel: 'root', + type: 'application/json', + href: `${url.origin}${url.pathname}` + }, + { + rel: 'self', + type: 'application/json', + href: url.toString() } + ]; - if (Number(offset) > 0) { - const previoustUrl = new URL(url.toString()); - previoustUrl.searchParams.set('limit', limit.toString()); - previoustUrl.searchParams.set('offset', (Number(offset) - Number(limit)).toString()); + if (styles.length === Number(limit)) { + links.push({ + rel: 'next', + type: 'application/json', + href: nextUrl.toString() + }); + } - links.push({ - rel: 'previous', - type: 'application/json', - href: previoustUrl.toString() - }); - } + if (Number(offset) > 0) { + const previoustUrl = new URL(url.toString()); + previoustUrl.searchParams.set('limit', limit.toString()); + previoustUrl.searchParams.set('offset', (Number(offset) - Number(limit)).toString()); - const totalCount = await getStyleCount(where, values); - let totalPages = Math.ceil(totalCount / Number(limit)); - if (totalPages === 0) { - totalPages = 1; - } - const styles: DashboardMapStyle[] = res.rows; - styles.forEach((s) => { - s.links = createStyleLinks(s, url); + links.push({ + rel: 'previous', + type: 'application/json', + href: previoustUrl.toString() }); + } - const currentPage = pageNumber(totalCount, Number(limit), Number(offset)); - const pages: Pages = { - totalCount, - totalPages, - currentPage - }; - - return new Response(JSON.stringify({ styles, links, pages })); - } catch (err) { - error(500, err); - } finally { - dbm.end(); + const totalCount = await getStyleCount(where); + let totalPages = Math.ceil(totalCount / Number(limit)); + if (totalPages === 0) { + totalPages = 1; } + styles.forEach((s) => { + s.links = createStyleLinks(s, url); + }); + + const currentPage = pageNumber(totalCount, Number(limit), Number(offset)); + const pages: Pages = { + totalCount, + totalPages, + currentPage + }; + + return new Response(JSON.stringify({ styles, links, pages })); }; /** @@ -331,10 +330,10 @@ export const POST: RequestHandler = async ({ request, url, locals }) => { Object.keys(styleJson.sources).forEach((key) => { const source = styleJson.sources[key]; - if ('url' in source && source.url.startsWith(url.origin)) { + if ('url' in source && source.url?.startsWith(url.origin)) { source.url = source.url.replace(url.origin, ''); } else if ('tiles' in source) { - source.tiles.forEach((tile) => { + source.tiles?.forEach((tile) => { if (tile.startsWith(url.origin)) { tile = tile.replace(url.origin, ''); } @@ -348,42 +347,38 @@ export const POST: RequestHandler = async ({ request, url, locals }) => { is_superuser = await isSuperuser(user_email); } - let styleId: number; - - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - try { - const query = { - text: `INSERT INTO geohub.style (name, style, layers, access_level, created_user) VALUES ($1, $2, $3, $4, $5) returning id`, - values: [ - body.name, - JSON.stringify(styleJson), - JSON.stringify(body.layers), - body.access_level, - session.user.email - ] - }; - - const res = await client.query(query); - if (res.rowCount === 0) { + let styleId: number | undefined; + await db.transaction(async (tx) => { + const res = await tx + .insert(styleInGeohub) + .values({ + name: body.name, + style: styleJson, + layers: body.layers, + accessLevel: body.access_level, + createdUser: session.user.email + }) + .returning({ id: styleInGeohub.id }); + + if (res.length === 0) { error(500, { message: 'failed to insert to the database.' }); } - styleId = res.rows[0].id; + styleId = res[0].id; // add style_permission for created user as owner const spm = new StylePermissionManager(styleId, user_email); - await spm.register(client, { - style_id: `${styleId}`, - user_email, - permission: Permission.OWNER - }); - } catch (err) { - await dbm.transactionRollback(); - error(500, err); - } finally { - await dbm.transactionEnd(); + await spm.register( + { + style_id: `${styleId}`, + user_email, + permission: Permission.OWNER + }, + tx as TransactionSchema + ); + }); + if (!styleId) { + error(500, { message: 'failed to save style.' }); } - const style = await getStyleById(styleId, url, user_email, is_superuser); return new Response(JSON.stringify(style)); }; @@ -436,53 +431,48 @@ export const PUT: RequestHandler = async ({ request, url, locals }) => { } } - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const styleJson: StyleSpecification = body.style; + const styleJson: StyleSpecification = body.style; - // delete sky - if ('sky' in styleJson) { - delete styleJson.sky; - } + // delete sky + if ('sky' in styleJson) { + delete styleJson.sky; + } - Object.keys(styleJson.sources).forEach((key) => { - const source = styleJson.sources[key]; - if ('url' in source && source.url.startsWith(url.origin)) { - source.url = source.url.replace(url.origin, ''); - } else if ('tiles' in source) { - source.tiles.forEach((tile) => { - if (tile.startsWith(url.origin)) { - tile = tile.replace(url.origin, ''); - } - }); - } - }); + Object.keys(styleJson.sources).forEach((key) => { + const source = styleJson.sources[key]; + if ('url' in source && source.url?.startsWith(url.origin)) { + source.url = source.url.replace(url.origin, ''); + } else if ('tiles' in source) { + source.tiles?.forEach((tile) => { + if (tile.startsWith(url.origin)) { + tile = tile.replace(url.origin, ''); + } + }); + } + }); - const now = new Date().toISOString(); - const query = { - text: ` - UPDATE geohub.style - SET name=$1, style=$2, layers=$3, updatedat=$4::timestamptz, access_level=$5, updated_user=$6 - WHERE id=$7`, - values: [ - body.name, - JSON.stringify(styleJson), - JSON.stringify(body.layers), - now, - body.access_level, - session.user.email, - id - ] - }; - - await client.query(query); - - style = (await getStyleById(id, url, session?.user?.email, is_superuser)) as DashboardMapStyle; - return new Response(JSON.stringify(style)); - } catch (err) { - error(500, err); - } finally { - dbm.end(); - } + const now = new Date().toISOString(); + + const res = await db + .update(styleInGeohub) + .set({ + name: body.name, + style: styleJson, + layers: body.layers, + accessLevel: body.access_level, + updatedUser: session.user.email, + updatedat: now + }) + .where(eq(styleInGeohub.id, id)) + .returning({ id: styleInGeohub.id }); + + const styleId = res[0].id; + + style = (await getStyleById( + styleId, + url, + session?.user?.email, + is_superuser + )) as DashboardMapStyle; + return new Response(JSON.stringify(style)); }; diff --git a/sites/geohub/src/routes/api/style/[id]/+server.ts b/sites/geohub/src/routes/api/style/[id]/+server.ts index 4dd7ab1e9..ea346d661 100644 --- a/sites/geohub/src/routes/api/style/[id]/+server.ts +++ b/sites/geohub/src/routes/api/style/[id]/+server.ts @@ -1,10 +1,12 @@ import type { RequestHandler } from './$types'; import { getStyleById, isSuperuser } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; import type { DashboardMapStyle } from '$lib/types'; import { getDomainFromEmail } from '$lib/helper'; import { AccessLevel, Permission } from '$lib/config/AppConfig'; import { error } from '@sveltejs/kit'; +import { styleInGeohub } from '$lib/server/schema'; +import { db } from '$lib/server/db'; +import { eq } from 'drizzle-orm'; export const GET: RequestHandler = async ({ params, locals, url }) => { const session = await locals.auth(); @@ -89,25 +91,9 @@ export const DELETE: RequestHandler = async ({ params, url, locals }) => { } } - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - try { - const query = { - text: `DELETE FROM geohub.style WHERE id = $1`, - values: [styleId] - }; - - const res = await client.query(query); - if (res.rowCount === 0) { - error(404, { message: `${styleId} does not exist in the database` }); - } - return new Response(undefined, { - status: 204 - }); - } catch (err) { - dbm.transactionRollback(); - error(500, err); - } finally { - dbm.transactionEnd(); - } + await db.delete(styleInGeohub).where(eq(styleInGeohub.id, styleId)); + + return new Response(undefined, { + status: 204 + }); }; diff --git a/sites/geohub/src/routes/api/style/[id]/permission/+server.ts b/sites/geohub/src/routes/api/style/[id]/permission/+server.ts index c547dad22..365a3d387 100644 --- a/sites/geohub/src/routes/api/style/[id]/permission/+server.ts +++ b/sites/geohub/src/routes/api/style/[id]/permission/+server.ts @@ -1,51 +1,44 @@ import type { RequestHandler } from './$types'; -import { getStyleById, isSuperuser } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { error } from '@sveltejs/kit'; import { StylePermissionManager, type StylePermission } from '$lib/server/StylePermissionManager.ts'; +import { db } from '$lib/server/db'; +import { eq } from 'drizzle-orm'; +import { styleInGeohub } from '$lib/server/schema'; + +const styleExists = async (id: number) => { + const st = await db + .select({ id: styleInGeohub.id }) + .from(styleInGeohub) + .where(eq(styleInGeohub.id, id)); + return st && st.length > 0; +}; -export const GET: RequestHandler = async ({ params, locals, url }) => { +export const GET: RequestHandler = async ({ params, locals }) => { const session = await locals.auth(); if (!session) error(403, { message: 'Permission error' }); const user_email = session?.user.email; - let is_superuser = false; - if (user_email) { - is_superuser = await isSuperuser(user_email); - } - const id = parseInt(params.id); - const style = await getStyleById(id, url, user_email, is_superuser); + const style = await styleExists(id); if (!style) { error(404, { message: `No style found.` }); } - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const dpm = new StylePermissionManager(id, user_email); - const permissions = await dpm.getAll(client); + const dpm = new StylePermissionManager(id, user_email); + const permissions = await dpm.getAll(); - return new Response(JSON.stringify(permissions)); - } finally { - dbm.end(); - } + return new Response(JSON.stringify(permissions)); }; -export const POST: RequestHandler = async ({ params, locals, request, url }) => { +export const POST: RequestHandler = async ({ params, locals, request }) => { const session = await locals.auth(); if (!session) error(403, { message: 'Permission error' }); const user_email = session?.user.email; - let is_superuser = false; - if (user_email) { - is_superuser = await isSuperuser(user_email); - } - const id = parseInt(params.id); const body = await request.json(); @@ -68,34 +61,23 @@ export const POST: RequestHandler = async ({ params, locals, request, url }) => permission: body.permission }; - const style = await getStyleById(id, url, user_email, is_superuser); + const style = await styleExists(id); if (!style) { error(404, { message: `No style found.` }); } - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - try { - const dpm = new StylePermissionManager(id, user_email); - await dpm.register(client, permission); - const permissions = await dpm.getAll(client); + const dpm = new StylePermissionManager(id, user_email); + await dpm.register(permission); + const permissions = await dpm.getAll(); - return new Response(JSON.stringify(permissions)); - } finally { - dbm.transactionEnd(); - } + return new Response(JSON.stringify(permissions)); }; -export const PUT: RequestHandler = async ({ params, locals, request, url }) => { +export const PUT: RequestHandler = async ({ params, locals, request }) => { const session = await locals.auth(); if (!session) error(403, { message: 'Permission error' }); const user_email = session?.user.email; - let is_superuser = false; - if (user_email) { - is_superuser = await isSuperuser(user_email); - } - const id = parseInt(params.id); const body = await request.json(); @@ -122,22 +104,16 @@ export const PUT: RequestHandler = async ({ params, locals, request, url }) => { createdat: body.createdat }; - const style = await getStyleById(id, url, user_email, is_superuser); + const style = await styleExists(id); if (!style) { error(404, { message: `No style found.` }); } - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - try { - const dpm = new StylePermissionManager(id, user_email); - await dpm.update(client, permission); - const permissions = await dpm.getAll(client); + const dpm = new StylePermissionManager(id, user_email); + await dpm.update(permission); + const permissions = await dpm.getAll(); - return new Response(JSON.stringify(permissions)); - } finally { - dbm.transactionEnd(); - } + return new Response(JSON.stringify(permissions)); }; export const DELETE: RequestHandler = async ({ params, locals, url }) => { @@ -145,31 +121,21 @@ export const DELETE: RequestHandler = async ({ params, locals, url }) => { if (!session) error(403, { message: 'Permission error' }); const user_email = session?.user.email; - let is_superuser = false; - if (user_email) { - is_superuser = await isSuperuser(user_email); - } - const id = parseInt(params.id); + const target_email = url.searchParams.get('user_email'); if (!target_email) { error(400, { message: `query parameter of user_email is required.` }); } - const style = await getStyleById(id, url, user_email, is_superuser); + const style = await styleExists(id); if (!style) { error(404, { message: `No style found.` }); } - const dbm = new DatabaseManager(); - const client = await dbm.transactionStart(); - try { - const dpm = new StylePermissionManager(id, user_email); - await dpm.delete(client, target_email); - const permissions = await dpm.getAll(client); + const dpm = new StylePermissionManager(id, user_email); + await dpm.delete(target_email); + const permissions = await dpm.getAll(); - return new Response(JSON.stringify(permissions)); - } finally { - dbm.transactionEnd(); - } + return new Response(JSON.stringify(permissions)); }; diff --git a/sites/geohub/src/routes/api/style/[id]/star/+server.ts b/sites/geohub/src/routes/api/style/[id]/star/+server.ts index 31deeecbf..38848cf0c 100644 --- a/sites/geohub/src/routes/api/style/[id]/star/+server.ts +++ b/sites/geohub/src/routes/api/style/[id]/star/+server.ts @@ -1,7 +1,9 @@ import type { RequestHandler } from './$types'; import { getStyleStarCount } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; import { error } from '@sveltejs/kit'; +import { styleFavouriteInGeohub } from '$lib/server/schema'; +import { db } from '$lib/server/db'; +import { sql } from 'drizzle-orm'; export const POST: RequestHandler = async ({ locals, params }) => { const session = await locals.auth(); @@ -13,44 +15,26 @@ export const POST: RequestHandler = async ({ locals, params }) => { const user_email = session.user.email; const now = new Date().toISOString(); - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: ` - INSERT INTO geohub.style_favourite ( - style_id, user_email, savedat - ) values ( - $1, - $2, - $3::timestamptz - ) - ON CONFLICT (style_id, user_email) - DO - UPDATE - SET - savedat=$3::timestamptz - `, - values: [style_id, user_email, now] - }; - - await client.query(query); - - const stars = await getStyleStarCount(client, style_id); - - const res = { - style_id, - user_email, - savedat: now, - no_stars: stars - }; - - return new Response(JSON.stringify(res)); - } catch (err) { - error(400, err); - } finally { - dbm.end(); - } + await db + .insert(styleFavouriteInGeohub) + .values({ styleId: style_id, userEmail: user_email, savedat: now }) + .onConflictDoUpdate({ + target: [styleFavouriteInGeohub.styleId, styleFavouriteInGeohub.userEmail], + set: { + savedat: now + } + }); + + const stars = await getStyleStarCount(style_id); + + const res = { + style_id, + user_email, + savedat: now, + no_stars: stars + }; + + return new Response(JSON.stringify(res)); }; export const DELETE: RequestHandler = async ({ locals, params }) => { @@ -62,27 +46,18 @@ export const DELETE: RequestHandler = async ({ locals, params }) => { const style_id = parseInt(params.id); const user_email = session.user.email; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: `DELETE FROM geohub.style_favourite WHERE style_id=$1 and user_email=$2`, - values: [style_id, user_email] - }; - - await client.query(query); + await db + .delete(styleFavouriteInGeohub) + .where( + sql`${styleFavouriteInGeohub.styleId} = ${style_id} AND ${styleFavouriteInGeohub.userEmail} = ${user_email}` + ); - const stars = await getStyleStarCount(client, style_id); + const stars = await getStyleStarCount(style_id); - const res = { - style_id, - no_stars: stars - }; + const res = { + style_id, + no_stars: stars + }; - return new Response(JSON.stringify(res)); - } catch (err) { - error(400, err); - } finally { - dbm.end(); - } + return new Response(JSON.stringify(res)); }; diff --git a/sites/geohub/src/routes/api/style/[id]/star/count/+server.ts b/sites/geohub/src/routes/api/style/[id]/star/count/+server.ts index 8941f13e2..943d04bb1 100644 --- a/sites/geohub/src/routes/api/style/[id]/star/count/+server.ts +++ b/sites/geohub/src/routes/api/style/[id]/star/count/+server.ts @@ -1,26 +1,15 @@ import type { RequestHandler } from './$types'; import { getStyleStarCount } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; export const GET: RequestHandler = async ({ params }) => { const style_id = parseInt(params.id); - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const stars = await getStyleStarCount(client, style_id); + const stars = await getStyleStarCount(style_id); - const res = { - style_id, - no_stars: stars - }; + const res = { + style_id, + no_stars: stars + }; - return new Response(JSON.stringify(res)); - } catch (err) { - return new Response(JSON.stringify({ message: err.message }), { - status: 400 - }); - } finally { - dbm.end(); - } + return new Response(JSON.stringify(res)); }; diff --git a/sites/geohub/src/routes/api/style/count/+server.ts b/sites/geohub/src/routes/api/style/count/+server.ts index c4271c0b4..0b2b7ce24 100644 --- a/sites/geohub/src/routes/api/style/count/+server.ts +++ b/sites/geohub/src/routes/api/style/count/+server.ts @@ -1,26 +1,13 @@ import type { RequestHandler } from './$types'; -import DatabaseManager from '$lib/server/DatabaseManager'; -import { error } from '@sveltejs/kit'; +import { db } from '$lib/server/db'; +import { styleInGeohub } from '$lib/server/schema'; +import { count } from 'drizzle-orm'; /** * Get the total count of styles stored in database * GET: ./api/style/count */ export const GET: RequestHandler = async () => { - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const query = { - text: `SELECT count(*) as count FROM geohub.style`, - values: [] - }; - - const res = await client.query(query); - - return new Response(JSON.stringify({ count: Number(res.rows[0].count) })); - } catch (err) { - error(400, err); - } finally { - dbm.end(); - } + const result = await db.select({ count: count() }).from(styleInGeohub); + return new Response(JSON.stringify(result.length === 0 ? 0 : result[0].count)); }; diff --git a/sites/geohub/src/routes/api/tags/+server.ts b/sites/geohub/src/routes/api/tags/+server.ts index 73ed64b02..cea5cb9f6 100644 --- a/sites/geohub/src/routes/api/tags/+server.ts +++ b/sites/geohub/src/routes/api/tags/+server.ts @@ -1,7 +1,8 @@ import type { RequestHandler } from './$types'; import type { Tag } from '$lib/types/Tag'; import { createDatasetSearchWhereExpression, isSuperuser } from '$lib/server/helpers'; -import DatabaseManager from '$lib/server/DatabaseManager'; +import { sql, type SQL } from 'drizzle-orm'; +import { db } from '$lib/server/db'; /** * Tags API - return available keys and values in tag table @@ -16,92 +17,84 @@ export const GET: RequestHandler = async ({ url, locals }) => { const session = await locals.auth(); const user_email = session?.user.email; - const dbm = new DatabaseManager(); - const client = await dbm.start(); - try { - const key = url.searchParams.get('key'); - const currentQueryUrl = url.searchParams.get('url'); + const key = url.searchParams.get('key'); + const currentQueryUrl = url.searchParams.get('url'); - let values = []; - if (key) { - values.push(key); + let whereSql: SQL | undefined = undefined; + if (currentQueryUrl) { + let is_superuser = false; + if (user_email) { + is_superuser = await isSuperuser(user_email); } + whereSql = await createDatasetSearchWhereExpression( + new URL(currentQueryUrl), + 'x', + is_superuser, + user_email + ); + } + + const sqlChunks: SQL[] = []; + sqlChunks.push( + sql.raw(` + WITH tag_count AS ( + SELECT z.key, z.value, COUNT(x.id) as count + FROM geohub.dataset x + INNER JOIN geohub.dataset_tag y + ON x.id = y.dataset_id + INNER JOIN geohub.tag z + ON y.tag_id = z.id + `) + ); + if (whereSql) { + sqlChunks.push(whereSql); + } - let whereSql = ''; - if (currentQueryUrl) { - let is_superuser = false; - if (user_email) { - is_superuser = await isSuperuser(user_email); - } - const whereExpressesion = await createDatasetSearchWhereExpression( - new URL(currentQueryUrl), - 'x', - is_superuser, - user_email - ); - whereSql = whereExpressesion.sql; - values = [...values, ...whereExpressesion.values]; + sqlChunks.push( + sql.raw(` + GROUP BY + z.key, z.value + ) + SELECT distinct x.key, x.value, y.count + FROM geohub.tag x + INNER JOIN tag_count y + ON x.key = y.key + AND x.value = y.value + WHERE EXISTS (SELECT id FROM geohub.dataset_tag WHERE tag_id = x.id) + ${ + !key + ? '' + : ` + AND x.key = '${key}' + ` } + ORDER BY x.key, x.value + `) + ); - const sql = { - text: ` - WITH tag_count AS ( - SELECT z.key, z.value, COUNT(x.id) as count - FROM geohub.dataset x - INNER JOIN geohub.dataset_tag y - ON x.id = y.dataset_id - INNER JOIN geohub.tag z - ON y.tag_id = z.id - ${whereSql} - GROUP BY - z.key, z.value - ) - SELECT distinct x.key, x.value, y.count - FROM geohub.tag x - INNER JOIN tag_count y - ON x.key = y.key - AND x.value = y.value - WHERE EXISTS (SELECT id FROM geohub.dataset_tag WHERE tag_id = x.id) - ${ - !key - ? '' - : ` - AND x.key = $1 - ` - } - ORDER BY x.key, x.value - `, - values: values - }; + const res: Tag[] = (await db.execute(sql.join(sqlChunks, sql.raw(' ')))) as unknown as Tag[]; - const res = await client.query(sql); - if (res.rowCount === 0) { - return new Response(JSON.stringify({})); - } + // const res = await client.query(sql); + if (res.length === 0) { + return new Response(JSON.stringify({})); + } - const result: { [key: string]: Tag[] } = {}; - res.rows.forEach((row) => { - if (!row.key) { - return; - } - if (!result[row.key]) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - result[row.key] = []; - } - result[row.key].push({ - key: row.key, - value: row.value, - count: Number(row.count) - }); + const result: { [key: string]: Tag[] } = {}; + res.forEach((row) => { + if (!row.key) { + return; + } + if (!result[row.key]) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + result[row.key] = []; + } + result[row.key].push({ + key: row.key, + value: row.value, + count: Number(row.count) }); + }); - return new Response(JSON.stringify(result)); - } catch (err) { - return new Response(JSON.stringify({ message: err.message }), { - status: 400 - }); - } finally { - dbm.end(); - } + return new Response(JSON.stringify(result)); }; diff --git a/sites/geohub/src/routes/api/users/+server.ts b/sites/geohub/src/routes/api/users/+server.ts index 5922cd03c..d13187e9c 100644 --- a/sites/geohub/src/routes/api/users/+server.ts +++ b/sites/geohub/src/routes/api/users/+server.ts @@ -1,6 +1,8 @@ import { error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; -import { searchUsersByEmail } from '$lib/server/helpers'; +import { db } from '$lib/server/db'; +import { like } from 'drizzle-orm'; +import { usersInGeohub } from '$lib/server/schema'; export const GET: RequestHandler = async ({ locals, url }) => { const session = await locals.auth(); @@ -17,7 +19,11 @@ export const GET: RequestHandler = async ({ locals, url }) => { const limit = url.searchParams.get('limit') ?? '10'; - const users = await searchUsersByEmail(query, parseInt(limit)); + const users = await db + .select({ id: usersInGeohub.id, user_email: usersInGeohub.userEmail }) + .from(usersInGeohub) + .where(like(usersInGeohub.userEmail, `%${query}%`)) + .limit(parseInt(limit)); return new Response(JSON.stringify(users)); };