diff --git a/deployment/development/Dockerfile.stg-tunnel b/deployment/development/Dockerfile.stg-tunnel new file mode 100644 index 0000000000..4bd8d1029e --- /dev/null +++ b/deployment/development/Dockerfile.stg-tunnel @@ -0,0 +1,2 @@ +FROM alpine +RUN apk --no-cache add openssh-client bash \ No newline at end of file diff --git a/deployment/development/docker-compose.yml b/deployment/development/docker-compose.yml index 60d3511621..8716ac0d53 100644 --- a/deployment/development/docker-compose.yml +++ b/deployment/development/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.4' +version: "3.4" # NOTE: that --project-directory ./source # must be passed to every docker-compose command @@ -13,8 +13,10 @@ volumes: ui-node_modules: ui-yarn-cache: postgres-data: + redis-data: outdir: + services: db: image: postgres:10 @@ -91,3 +93,31 @@ services: - ui-dist:/app/dist - ui-node_modules:/app/node_modules - ui-yarn-cache:/.yarn-cache + svelte-ui: + build: + context: ./source/SIL.AppBuilder.Portal/ + dockerfile: Dockerfile + ports: + - "5173:3000" + env_file: source/SIL.AppBuilder.Portal/.env + environment: + VITE_DATABASE_URL: "postgresql://db-user:1234@db:5432/development?schema=public" + # MUST be included (the path the application will be accessed on) and MUST NOT have a trailing slash + ORIGIN: "http://localhost:5173" + redis: + image: redis:latest + ports: + - "6379:6379" + command: redis-server --appendonly yes --appendfilename appendonly.aof --maxmemory-policy noeviction + volumes: + - redis-data:/data + stg-tunnel: + build: + context: ./deployment/development + dockerfile: Dockerfile.stg-tunnel + ports: + - "8443:8443" + # yikes + command: bash -c "cp -r /root/ssh /root/.ssh; chmod -R 600 /root/.ssh; ssh -N -L \*:8443:localhost:\$(ssh aps-stg \"docker ps --format '{{.Names}} {{.Ports}}' | grep -E 'buildengine.*web' | awk -F '->' '{print \\$1}' | awk -F ':' '{print \\$2}'\") aps-stg" + volumes: + - ~/.ssh:/root/ssh diff --git a/run b/run index 3ce44c68b9..5647deefc9 100755 --- a/run +++ b/run @@ -7,8 +7,9 @@ UI_PATH="source/SIL.AppBuilder.Portal.Frontend" API_PATH="source/OptimaJet.DWKit.StarterApplication" +SVELTE_PATH="source/SIL.AppBuilder.Portal" COMPOSE="docker compose -f deployment/development/docker-compose.yml --project-directory . -p appbuilder-portal" -COMPOSE_SERVICES="db adminer api ui" +COMPOSE_SERVICES="db adminer api ui svelte-ui redis stg-tunnel" CI_COMPOSE="docker compose -f deployment/ci/docker-compose.yml --project-directory . -p appbuilder-portal-ci" @@ -26,7 +27,7 @@ function runstuff { up:build) ${COMPOSE} up --build $arguments ${COMPOSE_SERVICES} ;; up) ${COMPOSE} up $arguments ${COMPOSE_SERVICES} ;; up:local:start) - runstuff dc up -d db adminer + runstuff dc up -d db adminer redis echo "db and adminer have been started. please make sure you have /etc/hosts entries" echo "" @@ -66,7 +67,25 @@ function runstuff { bootstrap) set -e + runstuff bootstrap:stop-api + runstuff bootstrap:drop + runstuff bootstrap:svelte + runstuff bootstrap:dwkit-data + runstuff bootstrap:restart-api + runstuff bootstrap:data + runstuff bootstrap:workflow + runstuff bootstrap:sample + runstuff bootstrap:dev + runstuff bootstrap:ui-static + set +e + ;; + + bootstrap:s1) + set -e + runstuff bootstrap:stop-api + runstuff bootstrap:drop runstuff bootstrap:dwkit + runstuff bootstrap:restart-api runstuff bootstrap:api runstuff bootstrap:sample runstuff bootstrap:dev @@ -84,11 +103,12 @@ function runstuff { set +e ;; - bootstrap:dwkit) + bootstrap:stop-api) echo "api must not be running in order to run the initial db scripts ..." runstuff dc stop api + ;; - + bootstrap:drop) # Setup Database ${COMPOSE} exec db bash -c "\ ${PSQL} -d postgres -tc \ @@ -98,28 +118,47 @@ function runstuff { && ${PSQL} -d postgres -c \"DROP DATABASE \$POSTGRES_DB\"" ${COMPOSE} exec db bash -c "${PSQL} -d postgres -c \"CREATE DATABASE \$POSTGRES_DB WITH ENCODING 'UTF8'\"" + ;; + bootstrap:dwkit) echo "Setting up DWKit..." - ${COMPOSE} exec db bash -c "${PSQL} -d \$POSTGRES_DB -f /scripts/PostgreSQL/DWKitScript.sql" \ - && ${COMPOSE} exec db bash -c "${PSQL} -d \$POSTGRES_DB -f /scripts/PostgreSQL/Workflow_CreatePersistenceObjects.sql" \ - && ${COMPOSE} exec db bash -c "${PSQL} -d \$POSTGRES_DB -f /scripts/PostgreSQL/DWKitUpdate_2.6.sql" \ - && echo "DB setup with DWKit Scripts" + runstuff bootstrap:dwkit-data + runstuff bootstrap:dwkit-migrate + echo "DB setup with DWKit Scripts" + ;; + bootstrap:dwkit-data) + ${COMPOSE} exec db bash -c "${PSQL} -d \$POSTGRES_DB -f /scripts/PostgreSQL/DWKitScript.sql" ;; - bootstrap:api) + bootstrap:dwkit-migrate) + ${COMPOSE} exec db bash -c "${PSQL} -d \$POSTGRES_DB -f /scripts/PostgreSQL/Workflow_CreatePersistenceObjects.sql" + ${COMPOSE} exec db bash -c "${PSQL} -d \$POSTGRES_DB -f /scripts/PostgreSQL/DWKitUpdate_2.6.sql" + ;; + + bootstrap:restart-api) echo "api must be running in order to run the db migration scripts ..." runstuff dc start api + ;; + bootstrap:api) echo "Running migrations..." + runstuff db:update + runstuff bootstrap:data + runstuff bootstrap:workflow + echo "DB setup for json:api" + ;; - runstuff db:update \ - && ${COMPOSE} exec db bash -c "${PSQL} -d \$POSTGRES_DB -f /scripts/bootstrap.sql" \ - && ${COMPOSE} exec db bash -c "${PSQL} -d \$POSTGRES_DB -f /scripts/default_workflow.sql" \ - && echo "DB setup for json:api" + bootstrap:data) + ${COMPOSE} exec db bash -c "${PSQL} -d \$POSTGRES_DB -f /scripts/bootstrap.sql" ;; bootstrap:workflow) - ${COMPOSE} exec db bash -c "${PSQL} -d \$POSTGRES_DB -f /scripts/default_workflow.sql" + ${COMPOSE} exec db bash -c "${PSQL} -d \$POSTGRES_DB -f /scripts/default_workflow.sql" + ;; + + bootstrap:svelte) + (cd $SVELTE_PATH \ + && npx prisma migrate dev --schema common/prisma/schema.prisma) ;; bootstrap:sample) diff --git a/scripts/DB/default_workflow.sql b/scripts/DB/default_workflow.sql index 9eee838803..d924947434 100644 --- a/scripts/DB/default_workflow.sql +++ b/scripts/DB/default_workflow.sql @@ -1,5 +1,5 @@ -INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Type", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId") VALUES -(1, 'sil_android_google_play', 1, '1', 'SIL Default Workflow for Publishing to Google Play', 'SIL_Default_AppBuilders_Android_GooglePlay', 'SIL_Default_AppBuilders_Android_GooglePlay_Flow', 1) +INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Type", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "WorkflowOptions") VALUES +(1, 'sil_android_google_play', 1, '1', 'SIL Default Workflow for Publishing to Google Play', 'SIL_Default_AppBuilders_Android_GooglePlay', 'SIL_Default_AppBuilders_Android_GooglePlay_Flow', 1, '{1, 2}') ON CONFLICT ("Id") DO UPDATE SET "Name" = excluded."Name", @@ -8,7 +8,8 @@ DO UPDATE SET "Description" = excluded."Description", "WorkflowScheme" = excluded."WorkflowScheme", "WorkflowBusinessFlow" = excluded."WorkflowBusinessFlow", - "StoreTypeId" = excluded."StoreTypeId"; + "StoreTypeId" = excluded."StoreTypeId", + "WorkflowOptions" = excluded."WorkflowOptions"; INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Type", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId") VALUES (2, 'sil_android_google_play_rebuild', 2, '1', 'SIL Default Workflow for Rebuilding to Google Play', 'SIL_Default_AppBuilders_Android_GooglePlay_Rebuild', 'SIL_Default_AppBuilders_Android_GooglePlay_Flow', 1) @@ -33,8 +34,8 @@ DO UPDATE SET "WorkflowBusinessFlow" = excluded."WorkflowBusinessFlow", "StoreTypeId" = excluded."StoreTypeId"; -INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Type", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId") VALUES -(4, 'sil_android_s3', 1, '1', 'SIL Default Workflow for Publish to Amazon S3 Bucket', 'SIL_Default_AppBuilders_Android_S3', 'SIL_Default_AppBuilders_Android_S3_Flow', 2) +INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Type", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "ProductType", "WorkflowOptions") VALUES +(4, 'sil_android_s3', 1, '1', 'SIL Default Workflow for Publish to Amazon S3 Bucket', 'SIL_Default_AppBuilders_Android_S3', 'SIL_Default_AppBuilders_Android_S3_Flow', 2, 1, '{2}') ON CONFLICT ("Id") DO UPDATE SET "Name" = excluded."Name", @@ -43,10 +44,12 @@ DO UPDATE SET "Description" = excluded."Description", "WorkflowScheme" = excluded."WorkflowScheme", "WorkflowBusinessFlow" = excluded."WorkflowBusinessFlow", - "StoreTypeId" = excluded."StoreTypeId"; + "StoreTypeId" = excluded."StoreTypeId", + "ProductType" = excluded."ProductType", + "WorkflowOptions" = excluded."WorkflowOptions"; -INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Type", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId") VALUES -(5, 'sil_android_s3_rebuild', 2, '1', 'SIL Default Workflow for Rebuilding to Amazon S3 Bucket', 'SIL_Default_AppBuilders_Android_S3_Rebuild', 'SIL_Default_AppBuilders_Android_S3_Flow', 2) +INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Type", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "ProductType") VALUES +(5, 'sil_android_s3_rebuild', 2, '1', 'SIL Default Workflow for Rebuilding to Amazon S3 Bucket', 'SIL_Default_AppBuilders_Android_S3_Rebuild', 'SIL_Default_AppBuilders_Android_S3_Flow', 2, 1) ON CONFLICT ("Id") DO UPDATE SET "Name" = excluded."Name", @@ -55,10 +58,11 @@ DO UPDATE SET "Description" = excluded."Description", "WorkflowScheme" = excluded."WorkflowScheme", "WorkflowBusinessFlow" = excluded."WorkflowBusinessFlow", - "StoreTypeId" = excluded."StoreTypeId"; + "StoreTypeId" = excluded."StoreTypeId", + "ProductType" = excluded."ProductType"; -INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type") VALUES -(6, 'la_android_google_play', '1', 'Low Admin Workflow for Publishing to Google Play', 'SIL_LowAdmin_AppBuilders_Android_GooglePlay', 'SIL_Default_AppBuilders_Android_GooglePlay_Flow', 1, 1) +INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type", "WorkflowOptions") VALUES +(6, 'la_android_google_play', '1', 'Low Admin Workflow for Publishing to Google Play', 'SIL_LowAdmin_AppBuilders_Android_GooglePlay', 'SIL_Default_AppBuilders_Android_GooglePlay_Flow', 1, 1, '{1}') ON CONFLICT ("Id") DO UPDATE SET "Name" = excluded."Name", @@ -67,7 +71,8 @@ DO UPDATE SET "WorkflowScheme" = excluded."WorkflowScheme", "WorkflowBusinessFlow" = excluded."WorkflowBusinessFlow", "StoreTypeId" = excluded."StoreTypeId", - "Type" = excluded."Type"; + "Type" = excluded."Type", + "WorkflowOptions" = excluded."WorkflowOptions"; INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type") VALUES (7, 'oa_android_google_play', '1', 'Owner Admin Workflow for Publishing to Google Play', 'SIL_OwnerAdmin_AppBuilders_Android_GooglePlay', 'SIL_Default_AppBuilders_Android_GooglePlay_Flow', 1, 1) @@ -81,8 +86,8 @@ DO UPDATE SET "StoreTypeId" = excluded."StoreTypeId", "Type" = excluded."Type"; -INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type") VALUES -(8, 'na_android_s3', '1', 'No Admin Workflow for Publishing to S3', 'SIL_NoAdmin_AppBuilders_Android_S3', 'SIL_Default_AppBuilders_Android_S3_Flow', 2, 1) +INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type", "ProductType") VALUES +(8, 'na_android_s3', '1', 'No Admin Workflow for Publishing to S3', 'SIL_NoAdmin_AppBuilders_Android_S3', 'SIL_Default_AppBuilders_Android_S3_Flow', 2, 1, 1) ON CONFLICT ("Id") DO UPDATE SET "Name" = excluded."Name", @@ -91,10 +96,11 @@ DO UPDATE SET "WorkflowScheme" = excluded."WorkflowScheme", "WorkflowBusinessFlow" = excluded."WorkflowBusinessFlow", "StoreTypeId" = excluded."StoreTypeId", - "Type" = excluded."Type"; + "Type" = excluded."Type", + "ProductType" = excluded."ProductType"; -INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type") VALUES -(9, 'pwa_cloud', '1', 'SIL Default Workflow for Publishing PWA to Cloud', 'SIL_Default_AppBuilders_Pwa_Cloud', 'SIL_AppBuilders_Web_Flow', 3, 1) +INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type", "Properties", "ProductType") VALUES +(9, 'pwa_cloud', '1', 'SIL Default Workflow for Publishing PWA to Cloud', 'SIL_Default_AppBuilders_Pwa_Cloud', 'SIL_AppBuilders_Web_Flow', 3, 1, '{ "build:targets" : "pwa" }', 3) ON CONFLICT ("Id") DO UPDATE SET "Name" = excluded."Name", @@ -103,10 +109,11 @@ DO UPDATE SET "WorkflowScheme" = excluded."WorkflowScheme", "WorkflowBusinessFlow" = excluded."WorkflowBusinessFlow", "StoreTypeId" = excluded."StoreTypeId", - "Type" = excluded."Type"; + "Type" = excluded."Type", + "ProductType" = excluded."ProductType"; -INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type") VALUES -(10, 'pwa_cloud_rebuild', '1', 'SIL Default Workflow for Rebuilding PWA to Cloud', 'SIL_Default_AppBuilders_Pwa_Cloud_Rebuild', 'SIL_AppBuilders_Web_Flow', 3, 2) +INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type", "Properties", "ProductType") VALUES +(10, 'pwa_cloud_rebuild', '1', 'SIL Default Workflow for Rebuilding PWA to Cloud', 'SIL_Default_AppBuilders_Pwa_Cloud_Rebuild', 'SIL_AppBuilders_Web_Flow', 3, 2, '{ "build:targets" : "pwa" }', 3) ON CONFLICT ("Id") DO UPDATE SET "Name" = excluded."Name", @@ -115,10 +122,11 @@ DO UPDATE SET "WorkflowScheme" = excluded."WorkflowScheme", "WorkflowBusinessFlow" = excluded."WorkflowBusinessFlow", "StoreTypeId" = excluded."StoreTypeId", - "Type" = excluded."Type"; + "Type" = excluded."Type", + "ProductType" = excluded."ProductType"; -INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type") VALUES -(11, 'html_cloud', '1', 'SIL Default Workflow for Publishing HTML to Cloud', 'SIL_Default_AppBuilders_Html_Cloud', 'SIL_AppBuilders_Web_Flow', 3, 1) +INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type", "ProductType") VALUES +(11, 'html_cloud', '1', 'SIL Default Workflow for Publishing HTML to Cloud', 'SIL_Default_AppBuilders_Html_Cloud', 'SIL_AppBuilders_Web_Flow', 3, 1, 3) ON CONFLICT ("Id") DO UPDATE SET "Name" = excluded."Name", @@ -127,10 +135,11 @@ DO UPDATE SET "WorkflowScheme" = excluded."WorkflowScheme", "WorkflowBusinessFlow" = excluded."WorkflowBusinessFlow", "StoreTypeId" = excluded."StoreTypeId", - "Type" = excluded."Type"; + "Type" = excluded."Type", + "ProductType" = excluded."ProductType"; -INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type") VALUES -(12, 'html_cloud_rebuild', '1', 'SIL Default Workflow for Rebuilding HTML to Cloud', 'SIL_Default_AppBuilders_Html_Cloud_Rebuild', 'SIL_AppBuilders_Web_Flow', 3, 2) +INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type", "ProductType") VALUES +(12, 'html_cloud_rebuild', '1', 'SIL Default Workflow for Rebuilding HTML to Cloud', 'SIL_Default_AppBuilders_Html_Cloud_Rebuild', 'SIL_AppBuilders_Web_Flow', 3, 2, 3) ON CONFLICT ("Id") DO UPDATE SET "Name" = excluded."Name", @@ -139,10 +148,11 @@ DO UPDATE SET "WorkflowScheme" = excluded."WorkflowScheme", "WorkflowBusinessFlow" = excluded."WorkflowBusinessFlow", "StoreTypeId" = excluded."StoreTypeId", - "Type" = excluded."Type"; + "Type" = excluded."Type", + "ProductType" = excluded."ProductType"; -INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type", "Properties") VALUES -(13, 'asset_package', '1', 'SIL Default Workflow for Publishing Asset Packages', 'SIL_NoAdmin_AppBuilders_Android_S3', 'SIL_AppBuilders_AssetPackage_Flow', 2, 1, '{ "build:targets" : "asset-package" }') +INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type", "ProductType") VALUES +(13, 'asset_package', '1', 'SIL Default Workflow for Publishing Asset Packages', 'SIL_NoAdmin_AppBuilders_Android_S3', 'SIL_AppBuilders_AssetPackage_Flow', 2, 1, 2) ON CONFLICT ("Id") DO UPDATE SET "Name" = excluded."Name", @@ -152,10 +162,11 @@ DO UPDATE SET "WorkflowBusinessFlow" = excluded."WorkflowBusinessFlow", "StoreTypeId" = excluded."StoreTypeId", "Type" = excluded."Type", - "Properties" = excluded."Properties"; + "Properties" = excluded."Properties", + "ProductType" = excluded."ProductType"; -INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type", "Properties") VALUES -(14, 'asset_package_rebuild', '1', 'SIL Default Workflow for Rebuilding Asset Packages', 'SIL_Default_AppBuilders_Android_S3_Rebuild', 'SIL_AppBuilders_AssetPackage_Flow', 2, 2, '{ "build:targets" : "asset-package" }') +INSERT INTO "WorkflowDefinitions" ("Id", "Name", "Enabled", "Description", "WorkflowScheme", "WorkflowBusinessFlow", "StoreTypeId", "Type", "ProductType") VALUES +(14, 'asset_package_rebuild', '1', 'SIL Default Workflow for Rebuilding Asset Packages', 'SIL_Default_AppBuilders_Android_S3_Rebuild', 'SIL_AppBuilders_AssetPackage_Flow', 2, 2, 2) ON CONFLICT ("Id") DO UPDATE SET "Name" = excluded."Name", @@ -165,7 +176,8 @@ DO UPDATE SET "WorkflowBusinessFlow" = excluded."WorkflowBusinessFlow", "StoreTypeId" = excluded."StoreTypeId", "Type" = excluded."Type", - "Properties" = excluded."Properties"; + "Properties" = excluded."Properties", + "ProductType" = excluded."ProductType"; INSERT INTO "ProductDefinitions" ("Id", "Name", "TypeId", "Description", "WorkflowId", "RebuildWorkflowId", "RepublishWorkflowId") VALUES (1, 'Android App to Google Play', 1, 'Build an Android App from a Scripture App Builder project and publish to a Google Play Store. The Organization Admin has to approve of the project and review the store preview. The Organization Admin has access to Google Play Console.', 1, 2, 3) diff --git a/scripts/DB/sample_data.sql b/scripts/DB/sample_data.sql index a36d6b9f95..4dd0983466 100644 --- a/scripts/DB/sample_data.sql +++ b/scripts/DB/sample_data.sql @@ -6,7 +6,10 @@ INSERT INTO "Users" ( ( 'Chris Hubbard (Kalaam)', 'chris.kalaam@gmail.com', 'auth0|5b578f6197af652b19f9bb41', 'Hubbard', 'Chris', '0', NULL, NULL, NULL, '0'), ( 'Bill Dyck', 'bill_dyck@sil.org', 'google-oauth2|102643649500459434996', 'Dyck', 'Bill', '0', NULL, NULL, NULL, '0'), ( 'Loren Hawthorne', 'loren_hawthrone@sil.org', 'google-oauth2|116603781884964961816', 'Hawthorne', 'Loren', '0', NULL, NULL, NULL, '0'), -( 'Chris Hubbard (BlueVire)', 'chris@bluevire.com', 'google-oauth2|108288848155985772105', 'Hubbard (BlueVire)', 'Chris', '0', NULL, NULL, NULL, '0'); +( 'Chris Hubbard (BlueVire)', 'chris@bluevire.com', 'google-oauth2|108288848155985772105', 'Hubbard (BlueVire)', 'Chris', '0', NULL, NULL, NULL, '0'), +( 'Micah Henney', '7dev7urandom@gmail.com', 'google-oauth2|102633638937992588080', 'Henney', 'Micah', '0', NULL, NULL, NULL, '0'), +( 'Aidan Jones', 'aejones4gm@gmail.com', 'google-oauth2|108677489047994521292', 'Jones', 'Aidan', '0', NULL, NULL, NULL, '0'), +( 'Aidan Jones (Alt)', 'ogonivanovich@gmail.com', 'google-oauth2|116630189316407820782', 'Jones', 'Aidan (Alt)', '0', NULL, NULL, NULL, '0'); INSERT INTO "Organizations" ("Id", "Name", "WebsiteUrl", "BuildEngineUrl", "BuildEngineApiAccessToken", "OwnerId", "UseDefaultBuildEngine") VALUES (1, 'SIL International', 'https://sil.org', 'https://dev-buildengine.scriptoria.io:8443', 'replace', 1, false), @@ -84,7 +87,10 @@ INSERT INTO "OrganizationMemberships" ("UserId", "OrganizationId") VALUES ( 5, 1), -- bill_dyck@sil.org - SIL ( 5, 3), -- bill_dyck@sil.org - Kalaam ( 6, 1), -- loren_hawthorne@sil.org - SIL -( 7, 1); -- chris@bluevire.com - SIL +( 7, 1), -- chris@bluevire.com - SIL +( 8, 1), -- 7dev7urandom@gmail.com - SIL +( 9, 1), -- aejones4gm@gmail.com - SIL +( 10, 1); -- ogonivanovich@gmail.com - SIL INSERT INTO "GroupMemberships" ("UserId", "GroupId") VALUES ( 1, 1), -- chris_hubbard@sil.org - LSDEV @@ -92,7 +98,10 @@ INSERT INTO "GroupMemberships" ("UserId", "GroupId") VALUES ( 2, 2), -- david_moore1@sil.org - CHB ( 2, 15), -- david_moore1@sil.org - KAL_AF ( 3, 14), -- lt.sego@gmail.com - Development (DT) -( 7, 1); -- chris@bluevire.com - LSDEV +( 7, 1), -- chris@bluevire.com - LSDEV +( 8, 1), -- 7dev7urandom@gmail.com - LSDEV +( 9, 1), -- aejones4gm@gmail.com -LSDev +( 10, 1); -- ogonivanovich@gmail.com -LSDev INSERT INTO "UserRoles" ("UserId", "RoleId", "OrganizationId") VALUES ( 1, 1, 1), -- chris_hubbard@sil.org - SuperAdmin - SIL @@ -105,7 +114,10 @@ INSERT INTO "UserRoles" ("UserId", "RoleId", "OrganizationId") VALUES ( 5, 3, 3), -- bill_dyck@sil.org - AppBuilder - Kalaam ( 2, 2, 3), -- david_moore1@sil.org - OrgAdmin - Kalaam ( 4, 3, 3), -- chris.kalaam@gmail.com - AppBuilder - Kalaam -( 7, 3, 1); -- chris@bluevire.com - AppBuilder - Kalaam +( 7, 3, 1), -- chris@bluevire.com - AppBuilder - Kalaam +( 8, 1, 1), -- 7dev7urandom@gmail.com - SuperAdmin - SIL +( 9, 1, 1), -- aejones4gm@gmail.com - SuperAdmin - SIL +( 10, 3, 1); -- ogonivanovich@gmail.com - AppBuilder - SIL SELECT SETVAL('"UserRoles_Id_seq"', COALESCE(MAX("Id"), 1) ) FROM "UserRoles"; diff --git a/source/SIL.AppBuilder.Portal/.dockerignore b/source/SIL.AppBuilder.Portal/.dockerignore new file mode 100644 index 0000000000..d499b6c8e7 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/.dockerignore @@ -0,0 +1,2 @@ +Dockerfile +node_modules diff --git a/source/SIL.AppBuilder.Portal/.eslintignore b/source/SIL.AppBuilder.Portal/.eslintignore new file mode 100644 index 0000000000..38972655fa --- /dev/null +++ b/source/SIL.AppBuilder.Portal/.eslintignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/source/SIL.AppBuilder.Portal/.eslintrc.cjs b/source/SIL.AppBuilder.Portal/.eslintrc.cjs new file mode 100644 index 0000000000..4fa1107c8b --- /dev/null +++ b/source/SIL.AppBuilder.Portal/.eslintrc.cjs @@ -0,0 +1,34 @@ +module.exports = { + root: true, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:svelte/recommended', + 'prettier' + ], + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020, + extraFileExtensions: ['.svelte'] + }, + env: { + browser: true, + es2017: true, + node: true + }, + overrides: [ + { + files: ['*.svelte'], + parser: 'svelte-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser' + } + } + ], + rules: { + '@typescript-eslint/no-unused-vars': 1, + indent: ['warn', 2] + } +}; diff --git a/source/SIL.AppBuilder.Portal/.gitignore b/source/SIL.AppBuilder.Portal/.gitignore new file mode 100644 index 0000000000..b456b347bc --- /dev/null +++ b/source/SIL.AppBuilder.Portal/.gitignore @@ -0,0 +1,12 @@ +.DS_Store +node_modules +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +src/lib/langtags.json +node-server/**/*.js +common/**/*.js diff --git a/source/SIL.AppBuilder.Portal/.npmrc b/source/SIL.AppBuilder.Portal/.npmrc new file mode 100644 index 0000000000..b6f27f1359 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/source/SIL.AppBuilder.Portal/.prettierignore b/source/SIL.AppBuilder.Portal/.prettierignore new file mode 100644 index 0000000000..604074e6ae --- /dev/null +++ b/source/SIL.AppBuilder.Portal/.prettierignore @@ -0,0 +1,12 @@ +.DS_Store +node_modules +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/source/SIL.AppBuilder.Portal/.prettierrc b/source/SIL.AppBuilder.Portal/.prettierrc new file mode 100644 index 0000000000..611b4c2e4e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/.prettierrc @@ -0,0 +1,11 @@ +{ + "useTabs": false, + "tabWidth": 2, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "pluginSearchDirs": ["."], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }], + "htmlWhitespaceSensitivity": "ignore" +} diff --git a/source/SIL.AppBuilder.Portal/Dockerfile b/source/SIL.AppBuilder.Portal/Dockerfile new file mode 100644 index 0000000000..6b36596950 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/Dockerfile @@ -0,0 +1,49 @@ +# Temporary container to build output +FROM node:22-alpine3.19 AS builder + +WORKDIR /build + +# Run npm i before copying all source code because Docker caches each layer +# and reuses them if nothing has changed. This way if package.json is unchanged, +# docker will skip the install even if other source files have changed. +COPY package*.json /build/ +COPY common/package*.json /build/common/ +RUN npm i + +# Run prisma generate to rebuild with the correct target, also caching +COPY common/prisma /build/common/ +RUN (cd common; npx prisma generate) + +# Copy all source and run a build +COPY . /build/ +RUN npm run build + +# Now the output is in /build/out/build +# Build the node-server code (output goes into /build/out as defined in tsconfig.json) +RUN (cd /build/common/; npx tsc) +RUN (cd /build/node-server/; npx tsc) + + +# Real container that will run +FROM node:22-alpine3.19 + +WORKDIR /app + +# Bring in package.json and install deps +COPY --from=builder /build/package*.json /app/ +COPY --from=builder /build/common/package*.json /app/common/ + +# Install production dependencies +RUN npm ci --omit dev + +# Bring in source code +COPY --from=builder /build/out /app + +# Bring in the common module +COPY --from=builder /build/common /app/common + +# Copy prisma data (npm ci nukes node_modules, so this must be last) +COPY --from=builder /build/node_modules/.prisma /app/node_modules/.prisma + +EXPOSE 3000 +CMD ["node", "/app/index.js"] diff --git a/source/SIL.AppBuilder.Portal/README.md b/source/SIL.AppBuilder.Portal/README.md new file mode 100644 index 0000000000..5c91169b0c --- /dev/null +++ b/source/SIL.AppBuilder.Portal/README.md @@ -0,0 +1,38 @@ +# create-svelte + +Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npm create svelte@latest + +# create a new project in my-app +npm create svelte@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. diff --git a/source/SIL.AppBuilder.Portal/common/ReadonlyPrisma.ts b/source/SIL.AppBuilder.Portal/common/ReadonlyPrisma.ts new file mode 100644 index 0000000000..b2a5166aeb --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/ReadonlyPrisma.ts @@ -0,0 +1,32 @@ +// Credit to https://github.com/prisma/prisma-client-extensions/blob/main/readonly-client/script.ts + +import { Prisma } from '@prisma/client'; + +export const WRITE_METHODS = [ + 'create', + 'update', + 'upsert', + 'delete', + 'createMany', + 'updateMany', + 'deleteMany' +] as const; + +export const ReadonlyClient = Prisma.defineExtension({ + name: 'ReadonlyClient', + model: { + $allModels: Object.fromEntries( + WRITE_METHODS.map((method) => [ + method, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + function (args: never) { + throw new Error(`Calling the \`${method}\` method on a readonly client is not allowed`); + } + ]) + ) as { + [K in (typeof WRITE_METHODS)[number]]: ( + args: `Calling the \`${K}\` method on a readonly client is not allowed` + ) => never; + } + } +}); diff --git a/source/SIL.AppBuilder.Portal/common/build-engine-api/index.ts b/source/SIL.AppBuilder.Portal/common/build-engine-api/index.ts new file mode 100644 index 0000000000..4bc332396a --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/build-engine-api/index.ts @@ -0,0 +1,2 @@ +export * as Types from './types.js'; +export * as Requests from './requests.js'; diff --git a/source/SIL.AppBuilder.Portal/common/build-engine-api/requests.ts b/source/SIL.AppBuilder.Portal/common/build-engine-api/requests.ts new file mode 100644 index 0000000000..63c1125367 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/build-engine-api/requests.ts @@ -0,0 +1,252 @@ +import prisma from '../prisma.js'; +import * as Types from './types.js'; + +export async function request( + resource: string, + auth: Types.Auth, + method: string = 'GET', + body?: unknown +) { + try { + const { url, token } = auth.type === 'query' ? await getURLandToken(auth.organizationId) : auth; + const check = await prisma.systemStatuses.findFirst({ + where: { + BuildEngineUrl: url, + BuildEngineApiAccessToken: token + }, + select: { + SystemAvailable: true + } + }); + if (!check?.SystemAvailable && resource !== 'system/check') { + return new Response( + JSON.stringify({ + responseType: 'error', + name: '', + status: 500, + code: 500, + message: `System ${url} unavailable`, + type: '' + } as Types.ErrorResponse), + { + status: 500, + statusText: 'Internal Server Error' + } + ); + } + return await fetch(`${url}/${resource}`, { + method: method, + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + 'Content-Type': 'application/json' + }, + body: body ? JSON.stringify(body) : undefined + }); + } catch (e) { + return new Response( + JSON.stringify({ + responseType: 'error', + name: '', + status: 500, + code: 500, + message: typeof e === 'string' ? e.toUpperCase() : e instanceof Error ? e.message : e, + type: '' + } as Types.ErrorResponse), + { + status: 500, + statusText: 'Internal Server Error' + } + ); + } +} +export async function getURLandToken(organizationId: number) { + const org = await prisma.organizations.findUnique({ + where: { + Id: organizationId + }, + select: { + BuildEngineUrl: true, + BuildEngineApiAccessToken: true, + UseDefaultBuildEngine: true + } + }); + + if (!org) { + throw new Error(`No organization could be found with ID: ${organizationId}`); + } + + return org.UseDefaultBuildEngine + ? { + url: process.env.DEFAULT_BUILDENGINE_URL, + token: process.env.DEFAULT_BUILDENGINE_API_ACCESS_TOKEN + } + : { + url: org.BuildEngineUrl, + token: org.BuildEngineApiAccessToken + }; +} + +export async function systemCheck(auth: Types.Auth) { + const res = await request('system/check', auth); + return res.ok + ? ({ status: res.status } as Types.StatusResponse) + : ((await res.json()) as Types.ErrorResponse); +} + +export async function createProject( + auth: Types.Auth, + project: Types.ProjectConfig +): Promise { + const res = await request('project', auth, 'POST', project); + return res.ok + ? ((await res.json()) as Types.ProjectResponse) + : ((await res.json()) as Types.ErrorResponse); +} +export async function getProjects( + auth: Types.Auth +): Promise { + const res = await request('project', auth); + return res.ok + ? ((await res.json()) as Types.ProjectResponse[]) + : ((await res.json()) as Types.ErrorResponse); +} +export async function getProject( + auth: Types.Auth, + projectId: number +): Promise { + const res = await request(`project/${projectId}`, auth); + return res.ok + ? ((await res.json()) as Types.ProjectResponse) + : ((await res.json()) as Types.ErrorResponse); +} +export async function deleteProject( + auth: Types.Auth, + projectId: number +): Promise { + const res = await request(`project/${projectId}`, auth, 'DELETE'); + return res.ok + ? { responseType: 'delete', status: res.status } + : ((await res.json()) as Types.ErrorResponse); +} + +export async function getProjectAccessToken( + auth: Types.Auth, + projectId: number, + token: Types.TokenConfig +): Promise { + const res = await request(`project/${projectId}/token`, auth, 'POST', token); + return res.ok + ? ((await res.json()) as Types.TokenResponse) + : ((await res.json()) as Types.ErrorResponse); +} + +export async function createJob( + auth: Types.Auth, + job: Types.JobConfig +): Promise { + const res = await request('job', auth, 'POST', job); + return res.ok + ? ((await res.json()) as Types.JobResponse) + : ((await res.json()) as Types.ErrorResponse); +} +export async function getJobs( + auth: Types.Auth +): Promise { + const res = await request('job', auth); + return res.ok + ? ((await res.json()) as Types.JobResponse[]) + : ((await res.json()) as Types.ErrorResponse); +} +export async function getJob( + auth: Types.Auth, + jobId: number +): Promise { + const res = await request(`job/${jobId}`, auth); + return res.ok + ? ((await res.json()) as Types.JobResponse) + : ((await res.json()) as Types.ErrorResponse); +} +export async function deleteJob( + auth: Types.Auth, + jobId: number +): Promise { + const res = await request(`job/${jobId}`, auth, 'DELETE'); + return res.ok + ? { responseType: 'delete', status: res.status } + : ((await res.json()) as Types.ErrorResponse); +} + +export async function createBuild( + auth: Types.Auth, + jobId: number, + build: Types.BuildConfig +): Promise { + const res = await request(`job/${jobId}/build`, auth, 'POST', build); + return res.ok + ? ((await res.json()) as Types.BuildResponse) + : ((await res.json()) as Types.ErrorResponse); +} +export async function getBuild( + auth: Types.Auth, + jobId: number, + buildId: number +): Promise { + const res = await request(`job/${jobId}/build/${buildId}`, auth); + return res.ok + ? ((await res.json()) as Types.BuildResponse) + : ((await res.json()) as Types.ErrorResponse); +} +export async function getBuilds( + auth: Types.Auth, + jobId: number +): Promise { + const res = await request(`job/${jobId}/build`, auth); + return res.ok + ? ((await res.json()) as Types.BuildResponse[]) + : ((await res.json()) as Types.ErrorResponse); +} +export async function deleteBuild( + auth: Types.Auth, + jobId: number, + buildId: number +): Promise { + const res = await request(`job/${jobId}/build/${buildId}`, auth, 'DELETE'); + return res.ok + ? { responseType: 'delete', status: res.status } + : ((await res.json()) as Types.ErrorResponse); +} + +export async function createRelease( + auth: Types.Auth, + jobId: number, + buildId: number, + release: Types.ReleaseConfig +): Promise { + const res = await request(`job/${jobId}/build/${buildId}`, auth, 'PUT', release); + return res.ok + ? ((await res.json()) as Types.ReleaseResponse) + : ((await res.json()) as Types.ErrorResponse); +} +export async function getRelease( + auth: Types.Auth, + jobId: number, + buildId: number, + releaseId: number +): Promise { + const res = await request(`job/${jobId}/build/${buildId}/release/${releaseId}`, auth); + return res.ok + ? ((await res.json()) as Types.ReleaseResponse) + : ((await res.json()) as Types.ErrorResponse); +} +export async function deleteRelease( + auth: Types.Auth, + jobId: number, + buildId: number, + releaseId: number +): Promise { + const res = await request(`job/${jobId}/build/${buildId}/release/${releaseId}`, auth, 'DELETE'); + return res.ok + ? { responseType: 'delete', status: res.status } + : ((await res.json()) as Types.ErrorResponse); +} diff --git a/source/SIL.AppBuilder.Portal/common/build-engine-api/types.ts b/source/SIL.AppBuilder.Portal/common/build-engine-api/types.ts new file mode 100644 index 0000000000..ecea5e2252 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/build-engine-api/types.ts @@ -0,0 +1,136 @@ +export type Auth = + | { + type: 'query'; + organizationId: number; + } + | { + type: 'provided'; + url: string; + token: string; + }; + +export type Response = + | ErrorResponse + | ProjectResponse + | TokenResponse + | BuildResponse + | ReleaseResponse + | StatusResponse; + +export type ErrorResponse = { + responseType: 'error'; + name: string; + message: string; + code: number; + status: number; + type: string; +}; + +export type StatusResponse = { + responseType: 'status'; + status: number; +}; + +export type DeleteResponse = { + responseType: 'delete'; + status: number; +}; + +type SuccessResponse = { + responseType: 'project' | 'token' | 'job' | 'build' | 'release'; + id: number; + created: Date; + updated: Date; + _links: { + self?: { + href: string; + }; + job?: { + href: string; + }; + }; +}; + +type CommonStatus = 'initialized' | 'accepted' | 'completed'; +type CommonResult = 'SUCCESS' | 'FAILURE' | null; + +export type ProjectConfig = { + app_id: string; + project_name: string; + language_code: string; + storage_type: string; +}; +export type ProjectResponse = SuccessResponse & + ProjectConfig & { + responseType: 'project'; + status: CommonStatus | 'delete' | 'deleting'; + result: CommonResult; + error: string | null; + url: string; + publishing_key: string; + user_id: string; + group_id: string; + }; + +export type TokenConfig = { + name: string; + ReadOnly: boolean; +}; +export type TokenResponse = { + responseType: 'token'; + SessionToken: string; + SecretAccessKey: string; + AccessKeyId: string; + Expiration: string; + Region: string; + ReadOnly: boolean; +}; + +export type JobConfig = { + request_id: string; + git_url: string; + app_id: string; + publisher_id: string; +}; +export type JobResponse = SuccessResponse & + JobConfig & { + responseType: 'job'; + }; + +type BuildOrReleaseStatus = 'active' | 'expired' | 'postprocessing'; + +type BuildCommon = { + targets: string; +}; +export type BuildConfig = BuildCommon & { + environment: { [key: string]: string }; +}; +export type BuildResponse = SuccessResponse & + BuildCommon & { + responseType: 'build'; + job_id: number; + status: CommonStatus | BuildOrReleaseStatus; + result: CommonResult | 'ABORTED'; + error: string | null; + artifacts: { [key: string]: string }; + }; + +export type Channels = 'production' | 'beta' | 'alpha'; + +type ReleaseCommon = { + channel: Channels; + targets: string; +}; +export type ReleaseConfig = ReleaseCommon & { + environment: { [key: string]: string }; +}; +export type ReleaseResponse = SuccessResponse & + ReleaseCommon & { + responseType: 'release'; + buildId: number; + status: CommonStatus | BuildOrReleaseStatus; + result: CommonResult | 'EXCEPTION'; + error: string | null; + consoleText: string; + artifacts: { [key: string]: string }; + }; diff --git a/source/SIL.AppBuilder.Portal/common/bullmq/queues.ts b/source/SIL.AppBuilder.Portal/common/bullmq/queues.ts new file mode 100644 index 0000000000..13b6981738 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/bullmq/queues.ts @@ -0,0 +1,40 @@ +import { Queue } from 'bullmq'; +import type { Job } from './types.js'; +import { QueueName } from './types.js'; + +/** Queue for Product Builds */ +export const Builds = new Queue(QueueName.Builds, { + connection: { + host: process.env.NODE_ENV === 'development' ? 'localhost' : 'redis' + } +}); +/** Queue for default recurring jobs such as the BuildEngine status check */ +export const DefaultRecurring = new Queue(QueueName.DefaultRecurring, { + connection: { + host: process.env.NODE_ENV === 'development' ? 'localhost' : 'redis' + } +}); +/** Queue for miscellaneous jobs such as Product and Project Creation */ +export const Miscellaneous = new Queue(QueueName.Miscellaneous, { + connection: { + host: process.env.NODE_ENV === 'development' ? 'localhost' : 'redis' + } +}); +/** Queue for Product Publishing */ +export const Publishing = new Queue(QueueName.Publishing, { + connection: { + host: process.env.NODE_ENV === 'development' ? 'localhost' : 'redis' + } +}); +/** Queue for jobs that poll BuildEngine, such as checking the status of a build */ +export const RemotePolling = new Queue(QueueName.RemotePolling, { + connection: { + host: process.env.NODE_ENV === 'development' ? 'localhost' : 'redis' + } +}); +/** Queue for operations on UserTasks */ +export const UserTasks = new Queue(QueueName.UserTasks, { + connection: { + host: process.env.NODE_ENV === 'development' ? 'localhost' : 'redis' + } +}); diff --git a/source/SIL.AppBuilder.Portal/common/bullmq/types.ts b/source/SIL.AppBuilder.Portal/common/bullmq/types.ts new file mode 100644 index 0000000000..8c5e8e68e6 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/bullmq/types.ts @@ -0,0 +1,212 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +import type { BuildResponse, Channels, ReleaseResponse } from '../build-engine-api/types.js'; +import { RoleId } from '../public/prisma.js'; + +interface RetryOptions { + readonly attempts: number; + readonly backoff: { + readonly type: string; + readonly delay: number; + }; +} +/** Maximum 5 attempts with a 5 second exponential backoff */ +export const Retry5e5: RetryOptions = { + attempts: 5, + backoff: { + type: 'exponential', + delay: 5000 // 5 seconds + } +}; + +interface RepeatOptions { + readonly repeat: { + readonly pattern: string; + }; +} +/** Repeat a job every minute */ +export const RepeatEveryMinute: RepeatOptions = { + repeat: { + pattern: '*/1 * * * *' // every minute + } +}; + +export enum QueueName { + Builds = 'Builds', + DefaultRecurring = 'Default Recurring', + Miscellaneous = 'Miscellaneous', + Publishing = 'Publishing', + RemotePolling = 'Remote Polling', + UserTasks = 'User Tasks' +} + +export enum JobType { + // Build Tasks + Build_Product = 'Build Product', + Build_Check = 'Check Product Build', + Build_PostProcess = 'Postprocess Build', + // Product Tasks + Product_Create = 'Create Product', + Product_Delete = 'Delete Product', + Product_GetVersionCode = 'Get VersionCode for Uploaded Product', + // Project Tasks + Project_Create = 'Create Project', + Project_Check = 'Check Project Creation', + Project_ImportProducts = 'Import Products for Project', + // Publishing Tasks + Publish_Product = 'Publish Product', + Publish_Check = 'Check Product Publish', + Publish_PostProcess = 'Postprocess Publish', + // System Tasks + System_CheckStatuses = 'Check System Statuses', + // UserTasks + UserTasks_Modify = 'Modify UserTasks' +} + +export namespace Build { + export interface Product { + type: JobType.Build_Product; + productId: string; + defaultTargets: string; + environment: { [key: string]: string }; + } + export interface Check { + type: JobType.Build_Check; + organizationId: number; + productId: string; + jobId: number; + buildId: number; + productBuildId: number; + } + export interface PostProcess { + type: JobType.Build_PostProcess; + productId: string; + productBuildId: number; + build: BuildResponse; + } +} + +export namespace Product { + export interface Create { + type: JobType.Product_Create; + productId: string; + } + export interface Delete { + type: JobType.Product_Delete; + organizationId: number; + workflowJobId: number; + } + export interface GetVersionCode { + type: JobType.Product_GetVersionCode; + productId: string; + } +} + +export namespace Project { + export interface Create { + type: JobType.Project_Create; + projectId: number; + } + + export interface Check { + type: JobType.Project_Check; + workflowProjectId: number; + organizationId: number; + projectId: number; + } + + export interface ImportProducts { + type: JobType.Project_ImportProducts; + organizationId: number; + importId: number; + } +} + +export namespace Publish { + export interface Product { + type: JobType.Publish_Product; + productId: string; + defaultChannel: Channels; + defaultTargets: string; + environment: { [key: string]: string }; + } + + export interface Check { + type: JobType.Publish_Check; + organizationId: number; + productId: string; + jobId: number; + buildId: number; + releaseId: number; + publicationId: number; + } + + export interface PostProcess { + type: JobType.Publish_PostProcess; + productId: string; + publicationId: number; + release: ReleaseResponse; + } +} + +export namespace System { + export interface CheckStatuses { + type: JobType.System_CheckStatuses; + } +} + +export namespace UserTasks { + export enum OpType { + Delete = 'Delete', + Update = 'Update', + Create = 'Create', + Reassign = 'Reassign' + } + type Config = + | { + type: OpType.Delete | OpType.Create | OpType.Update; + roles?: RoleId[]; + users?: number[]; + } + | { + type: OpType.Reassign; + userMapping: { from: number; to: number }[]; + roles?: never; + users?: never; + }; + + // Using type here instead of interface for easier composition + export type Modify = ( + | { + scope: 'Project'; + projectId: number; + } + | { + scope: 'Product'; + productId: string; + } + ) & { + type: JobType.UserTasks_Modify; + comment?: string; // just ignore comment for Delete and Reassign + operation: Config; + }; +} + +export type Job = JobTypeMap[keyof JobTypeMap]; + +export type JobTypeMap = { + [JobType.Build_Product]: Build.Product; + [JobType.Build_Check]: Build.Check; + [JobType.Build_PostProcess]: Build.PostProcess; + [JobType.Product_Create]: Product.Create; + [JobType.Product_Delete]: Product.Delete; + [JobType.Product_GetVersionCode]: Product.GetVersionCode; + [JobType.Project_Create]: Project.Create; + [JobType.Project_Check]: Project.Check; + [JobType.Project_ImportProducts]: Project.ImportProducts; + [JobType.Publish_Product]: Publish.Product; + [JobType.Publish_Check]: Publish.Check; + [JobType.Publish_PostProcess]: Publish.PostProcess; + [JobType.System_CheckStatuses]: System.CheckStatuses; + [JobType.UserTasks_Modify]: UserTasks.Modify; + // Add more mappings here as needed +}; diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/Authors.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/Authors.ts new file mode 100644 index 0000000000..0773c2fd1f --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/Authors.ts @@ -0,0 +1,9 @@ +import prisma from '../prisma.js'; + +export function deleteAuthor(authorId: number) { + return prisma.authors.delete({ + where: { + Id: authorId + } + }); +} diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/GroupMemberships.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/GroupMemberships.ts new file mode 100644 index 0000000000..f55b38101b --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/GroupMemberships.ts @@ -0,0 +1,69 @@ +import prisma from '../prisma.js'; + +// Make sure that all groups belong to organizations the user is in +// and removed groups are not linked to projects this user owns +export async function updateUserGroups(userId: number, groups: number[]): Promise { + const currentGroups = ( + await prisma.groupMemberships.findMany({ + where: { + UserId: userId + } + }) + ).map((g) => g.GroupId); + const removedGroups = currentGroups.filter((group) => !groups.includes(group)); + // Are there any removed groups that have a project this user owns + for (const group of removedGroups) { + if ( + ( + await prisma.projects.findMany({ + where: { + OwnerId: userId, + GroupId: group + } + }) + ).length > 0 + ) { + return false; + } + } + + const userOrganizations = await prisma.organizationMemberships.findMany({ + where: { + UserId: userId + } + }); + // Are there any groups we are setting that are not owned by one of the user's organizations + if ( + await prisma.groups.findFirst({ + where: { + Id: { + in: groups + }, + OwnerId: { + notIn: userOrganizations.map((o) => o.OrganizationId) + } + } + }) + ) + return false; + + // Verified, perform the changes + await prisma.$transaction([ + prisma.groupMemberships.deleteMany({ + where: { + GroupId: { + in: removedGroups + } + } + }), + prisma.groupMemberships.createMany({ + data: groups + .filter((group) => !currentGroups.includes(group)) + .map((group) => ({ + GroupId: group, + UserId: userId + })) + }) + ]); + return true; +} diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/Groups.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/Groups.ts new file mode 100644 index 0000000000..de66809362 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/Groups.ts @@ -0,0 +1,27 @@ +import prisma from '../prisma.js'; + +export async function update(): Promise { + throw new Error('Not implemented'); +} +export async function deleteGroup(id: number) { + if ( + await prisma.projects.findFirst({ + where: { + GroupId: id + } + }) + ) + return false; + await prisma.groups.delete({ where: { Id: id } }); + return true; +} +export async function createGroup(name: string, abbreviation: string, organization: number) { + await prisma.groups.create({ + data: { + OwnerId: organization, + Name: name, + Abbreviation: abbreviation + } + }); + return true; +} diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/OrganizationMemberships.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/OrganizationMemberships.ts new file mode 100644 index 0000000000..c7cb0a92a5 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/OrganizationMemberships.ts @@ -0,0 +1,78 @@ +import prisma from '../prisma.js'; + +export async function acceptOrganizationInvite(userId: number, inviteToken: string) { + const invite = await prisma.organizationMembershipInvites.findFirst({ + where: { + Token: inviteToken + } + }); + if (!invite || invite.Redeemed || !invite.Expires || invite.Expires < new Date()) return false; + // Check if the user is already a member of the organization + // This could be done such that they can accept the invite to get the roles and groups + const existingMembership = await prisma.organizationMemberships.findFirst({ + where: { + UserId: userId, + OrganizationId: invite.OrganizationId + } + }); + if (existingMembership) return false; + await prisma.organizationMemberships.create({ + data: { + UserId: userId, + OrganizationId: invite.OrganizationId + } + }); + await prisma.userRoles.createMany({ + data: invite.Roles.map((r) => ({ + UserId: userId, + RoleId: r, + OrganizationId: invite.OrganizationId + })) + }); + // Make sure that we don't try to add the user to groups that have since been deleted + const existingGroups = ( + await prisma.groups.findMany({ + where: { + OwnerId: invite.OrganizationId + } + }) + ).map((g) => g.Id); + await prisma.groupMemberships.createMany({ + data: invite.Groups.map((g) => ({ + UserId: userId, + GroupId: g + })).filter((l) => existingGroups.includes(l.GroupId)) + }); + + await prisma.organizationMembershipInvites.update({ + where: { + Id: invite.Id + }, + data: { + Redeemed: true + } + }); + return true; +} + +// Technically only modifies OrganizationMembershipInvites but we'll keep all membership functions together +export async function createOrganizationInvite( + email: string, + organizationId: number, + invitedById: number, + roles: number[], + groups: number[] +) { + // Note: this email is never used except to send the initial email. + // It sits in the database for reference sake only + const invite = await prisma.organizationMembershipInvites.create({ + data: { + InvitedById: invitedById, + Email: email, + OrganizationId: organizationId, + Roles: roles, + Groups: groups + } + }); + return invite.Token; +} diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/OrganizationProductDefinitions.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/OrganizationProductDefinitions.ts new file mode 100644 index 0000000000..42bcb5abb5 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/OrganizationProductDefinitions.ts @@ -0,0 +1,36 @@ +import prisma from '../prisma.js'; + +export async function updateOrganizationProductDefinitions( + orgId: number, + productDefinitions: number[] +) { + const old = ( + await prisma.organizationProductDefinitions.findMany({ + where: { + OrganizationId: orgId + } + }) + ).map((x) => x.ProductDefinitionId); + const newEntries = productDefinitions.filter( + (productDefinition) => !old.includes(productDefinition) + ); + const removeEntries = old.filter( + (productDefinition) => !productDefinitions.includes(productDefinition) + ); + await prisma.$transaction([ + prisma.organizationProductDefinitions.deleteMany({ + where: { + OrganizationId: orgId, + ProductDefinitionId: { + in: removeEntries + } + } + }), + prisma.organizationProductDefinitions.createMany({ + data: newEntries.map((store) => ({ + OrganizationId: orgId, + ProductDefinitionId: store + })) + }) + ]); +} diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/OrganizationStores.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/OrganizationStores.ts new file mode 100644 index 0000000000..7008106428 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/OrganizationStores.ts @@ -0,0 +1,29 @@ +import prisma from '../prisma.js'; + +export async function updateOrganizationStores(orgId: number, stores: number[]) { + const old = ( + await prisma.organizationStores.findMany({ + where: { + OrganizationId: orgId + } + }) + ).map((x) => x.StoreId); + const newEntries = stores.filter((store) => !old.includes(store)); + const removeEntries = old.filter((store) => !stores.includes(store)); + await prisma.$transaction([ + prisma.organizationStores.deleteMany({ + where: { + OrganizationId: orgId, + StoreId: { + in: removeEntries + } + } + }), + prisma.organizationStores.createMany({ + data: newEntries.map((store) => ({ + OrganizationId: orgId, + StoreId: store + })) + }) + ]); +} diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/Products.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/Products.ts new file mode 100644 index 0000000000..a8ae098341 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/Products.ts @@ -0,0 +1,244 @@ +import type { Prisma } from '@prisma/client'; +import { Workflow } from 'sil.appbuilder.portal.common'; +import { BullMQ, Queues } from '../index.js'; +import prisma from '../prisma.js'; +import type { RequirePrimitive } from './utility.js'; +import { delete as deleteInstance } from './WorkflowInstances.js'; + +export async function create( + productData: RequirePrimitive +): Promise { + if ( + !(await validateProductBase( + productData.ProjectId, + productData.ProductDefinitionId, + productData.StoreId ?? undefined, + productData.StoreLanguageId ?? undefined + )) + ) + return false; + + // No additional verification steps + + try { + const res = await prisma.products.create({ + data: productData + }); + + if (res) { + const flowDefinition = ( + await prisma.productDefinitions.findUnique({ + where: { + Id: productData.ProductDefinitionId + }, + select: { + Workflow: { + select: { + Id: true, + Type: true, + ProductType: true, + WorkflowOptions: true + } + } + } + }) + )?.Workflow; + + if (flowDefinition) { + await Workflow.create(res.Id, { + productType: flowDefinition.ProductType, + options: new Set(flowDefinition.WorkflowOptions), + workflowType: flowDefinition.Type + }); + } + } + + return res.Id; + } catch (e) { + return false; + } +} + +export async function update( + id: string, + productData: RequirePrimitive +): Promise { + // There are cases where a db lookup is not necessary to verify that it will + // be a legal relation after the update, such as if none of the relevant + // columns are changed, but for simplicity we just lookup once anyway + const existing = await prisma.products.findUnique({ + where: { + Id: id + } + }); + const projectId = productData.ProjectId ?? existing!.ProjectId; + const productDefinitionId = productData.ProductDefinitionId ?? existing!.ProductDefinitionId; + const storeId = productData.StoreId ?? existing!.StoreId ?? undefined; + const storeLanguageId = productData.StoreLanguageId ?? existing!.StoreLanguageId ?? undefined; + if (!(await validateProductBase(projectId, productDefinitionId, storeId, storeLanguageId))) + return false; + + // No additional verification steps + + try { + await prisma.products.update({ + where: { + Id: id + }, + data: productData + }); + // TODO: Are there any other updates that need to be done? + } catch (e) { + return false; + } + return true; +} + +async function deleteProduct(productId: string) { + // Delete all userTasks for this product, and delete the product + const product = await prisma.products.findUnique({ + where: { + Id: productId + }, + select: { + Project: { + select: { + Id: true, + OrganizationId: true + } + }, + WorkflowJobId: true + } + }); + await Queues.Miscellaneous.add( + `Delete Product #${productId} from BuildEngine`, + { + type: BullMQ.JobType.Product_Delete, + organizationId: product!.Project.OrganizationId, + workflowJobId: product!.WorkflowJobId + }, + BullMQ.Retry5e5 + ); + return prisma.$transaction([ + deleteInstance(productId, product!.Project.Id), + prisma.userTasks.deleteMany({ + where: { + ProductId: productId + } + }), + prisma.products.delete({ + where: { + Id: productId + } + }) + ]); +} +export { deleteProduct as delete }; + +/** A product is valid if: + * 1. The store's type matches the Workflow's store type + * 2. The project has a WorkflowProjectUrl + * 3. The store is allowed by the organization + * 4. The language is allowed by the store + * 5. The product type is allowed by the organization + */ +async function validateProductBase( + projectId: number, + productDefinitionId: number, + /** If this would be `null`, it is set to `undefined` by caller */ + storeId?: number, + /** If this would be `null`, it is set to `undefined` by caller */ + storeLanguageId?: number +) { + if (storeId === undefined) { + return false; + } + const productDefinition = await prisma.productDefinitions.findUnique({ + where: { + Id: productDefinitionId + }, + select: { + Id: true, + // Store type must match Workflow store type + Workflow: { + select: { + StoreTypeId: true + } + } + } + }); + const project = await prisma.projects.findUnique({ + where: { + Id: projectId, + // Project must have a WorkflowProjectUrl (handled by query) + WorkflowProjectUrl: { + not: null + } + }, + select: { + Organization: { + select: { + // Store must be allowed by Organization + OrganizationStores: { + where: { + StoreId: storeId + }, + select: { + Store: { + select: { + StoreType: { + select: { + // Store type must match Workflow store type + Id: true, + // StoreLanguage must be allowed by Store, if the StoreLanguage is defined + StoreLanguages: + storeLanguageId === undefined + ? undefined + : { + where: { + Id: storeLanguageId + }, + select: { + Id: true + } + } + } + } + } + } + } + }, + // Product type must be allowed by Organization + OrganizationProductDefinitions: { + where: { + ProductDefinitionId: productDefinition?.Id + } + } + } + } + } + }); + + /** 3. The store is allowed by the organization */ + const storeInOrg = (project?.Organization.OrganizationStores.length ?? 0) > 0; + + const prodDefStore = productDefinition?.Workflow.StoreTypeId; + const orgStore = project?.Organization.OrganizationStores[0]?.Store.StoreType.Id; + /** 1. The store's type matches the Workflow's store type + * + * Note: if both are undefined, this would be `true`; however, under those circumstances, + * condition #3 would evaluate to `false`, rendering the whole check `false`. + */ + const storeMatchFlowStore = prodDefStore === orgStore; + + const numOrgStoreLangs = + project?.Organization.OrganizationStores[0]?.Store.StoreType.StoreLanguages.length; + /** 4. The language, if specified, is allowed by the store */ + const optionalLanguageAllowed = storeLanguageId === undefined || (numOrgStoreLangs ?? 0) > 0; + + const numOrgProdDefs = project?.Organization.OrganizationProductDefinitions.length; + /** 5. The product type is allowed by the organization */ + const productInOrg = (numOrgProdDefs ?? 0) > 0; + + return storeInOrg && storeMatchFlowStore && optionalLanguageAllowed && productInOrg; +} diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/Projects.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/Projects.ts new file mode 100644 index 0000000000..a4cd5d2d52 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/Projects.ts @@ -0,0 +1,135 @@ +import type { Prisma } from '@prisma/client'; +import { BullMQ, Queues } from '../index.js'; +import prisma from '../prisma.js'; +import { RoleId } from '../public/prisma.js'; +import type { RequirePrimitive } from './utility.js'; + +/** + * For a project to be valid: + * 1. Each of the following must reference a valid relation (HANDLED BY POSTGRESQL) + * - OrganizationId => Organizations + * - GroupId => Groups + * - OwnerId => Users + * 2. This project's group must have the same organization as the project itself + * - Group.OwnerId === OrganizationId + * 3. The project's owner must be in the project's organization + * - Owner.OrganizationMemberships[].OrganizationId includes OrganizationId + */ + +export async function create( + projectData: RequirePrimitive +): Promise { + if ( + !(await validateProjectBase( + projectData.OrganizationId, + projectData.GroupId, + projectData.OwnerId + )) + ) + return false; + + // No additional verification steps + + try { + const res = await prisma.projects.create({ + data: projectData + }); + return res.Id; + } catch (e) { + return false; + } +} + +export async function update( + id: number, + projectData: RequirePrimitive +): Promise { + // There are cases where a db lookup is not necessary to verify that it will + // be a legal relation after the update, such as if none of the relevant + // columns are changed, but for simplicity we just lookup once anyway + const existing = await prisma.projects.findUnique({ + where: { + Id: id + } + }); + const orgId = projectData.OrganizationId ?? existing!.OrganizationId; + const groupId = projectData.GroupId ?? existing!.GroupId; + const ownerId = projectData.OwnerId ?? existing!.OwnerId; + if (!(await validateProjectBase(orgId, groupId, ownerId))) return false; + + // No additional verification steps + + try { + await prisma.projects.update({ + where: { + Id: id + }, + data: projectData + }); + // If the owner has changed, we need to reassign all the user tasks related to this project + if (ownerId && ownerId !== existing?.OwnerId) { + await Queues.UserTasks.add(`Reassign tasks for Project #${id} (New Owner)`, { + type: BullMQ.JobType.UserTasks_Modify, + scope: 'Project', + projectId: id, + operation: { + type: BullMQ.UserTasks.OpType.Reassign, + userMapping: [{ from: existing!.OwnerId, to: ownerId }] + } + }); + } + } catch (e) { + return false; + } + return true; +} + +export async function createMany(projectData: RequirePrimitive[]) { + const valid = ( + await Promise.all( + projectData.map((pd) => validateProjectBase(pd.OrganizationId, pd.GroupId, pd.OwnerId)) + ) + ).reduce((p, c) => p && c, true); + + try { + if (valid) { + return ( + await prisma.projects.createManyAndReturn({ data: projectData, select: { Id: true } }) + ).map((p) => p.Id); + } + } catch (e) { + return false; + } + + return false; +} + +// async function deleteProject(id: number): Promise { +// throw new Error('Should not be deleting a project, only archiving'); +// } +// export { deleteProject as delete }; + +async function validateProjectBase(orgId: number, groupId: number, ownerId: number) { + // Each of the criteria for a valid project just needs to checked if + // the relevant data is supplied. If it isn't, then this is an update + // and the data was valid already, or PostgreSQL will catch it + /** owner must be a member of project group */ + const userInGroup = await prisma.groupMemberships.count({ + where: { UserId: ownerId, GroupId: groupId } + }); + /** owner must be a member of project org */ + const userInOrg = await prisma.organizationMemberships.count({ + where: { UserId: ownerId, OrganizationId: orgId } + }); + /** disregard owner restrictions if owner is Super Admin */ + const userIsSuperAdmin = await prisma.userRoles.count({ + where: { RoleId: RoleId.SuperAdmin, UserId: ownerId } + }); + return !!( + // project group must be owned by project org + ( + orgId === (await prisma.groups.findUnique({ where: { Id: groupId } }))?.OwnerId && + ((userInGroup && userInOrg) || userIsSuperAdmin) + ) + ); +} diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/UserRoles.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/UserRoles.ts new file mode 100644 index 0000000000..eae577ebb2 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/UserRoles.ts @@ -0,0 +1,131 @@ +import { BullMQ, Queues } from '../index.js'; +import prisma from '../prisma.js'; +import { RoleId } from '../public/prisma.js'; + +export async function setUserRolesForOrganization( + userId: number, + organizationId: number, + roles: RoleId[] +) { + const old = ( + await prisma.userRoles.findMany({ + where: { + UserId: userId, + OrganizationId: organizationId + } + }) + ).map((r) => r.RoleId); + const remove = old.filter((role) => !roles.includes(role)); + const add = roles.filter((role) => !old.includes(role)); + await prisma.$transaction([ + prisma.userRoles.deleteMany({ + where: { + UserId: userId, + OrganizationId: organizationId, + RoleId: { + in: remove + } + } + }), + prisma.userRoles.createMany({ + data: add.map((r) => ({ + UserId: userId, + OrganizationId: organizationId, + RoleId: r + })) + }) + ]); + if (remove.includes(RoleId.OrgAdmin) || add.includes(RoleId.OrgAdmin)) { + const projects = ( + await prisma.projects.findMany({ + where: { + OrganizationId: organizationId + }, + select: { + Id: true + } + }) + ).map((p) => p.Id); + + const del = remove.includes(RoleId.OrgAdmin); + await Queues.UserTasks.addBulk( + projects.map((pid) => ({ + name: `${del ? 'Remove' : 'Add'} OrgAdmin tasks for User #${userId} on Project #${pid}`, + data: { + type: BullMQ.JobType.UserTasks_Modify, + scope: 'Project', + projectId: pid, + operation: { + type: del ? BullMQ.UserTasks.OpType.Delete : BullMQ.UserTasks.OpType.Create, + users: [userId], + roles: [RoleId.OrgAdmin] + } + } + })) + ); + } +} + +export async function allUsersByRole( + projectId: number, + roles?: RoleId[] +): Promise>> { + const project = await prisma.projects.findUnique({ + where: { + Id: projectId + }, + select: { + OrganizationId: true, + OwnerId: !roles || roles.includes(RoleId.AppBuilder), + Authors: + !roles || roles.includes(RoleId.Author) + ? { + select: { + UserId: true + } + } + : undefined + } + }); + + + if (!project) return {}; + + const admins = + !roles || roles.includes(RoleId.OrgAdmin) + ? await prisma.userRoles.findMany({ + where: { + OrganizationId: project.OrganizationId, + RoleId: RoleId.OrgAdmin + }, + select: { + UserId: true + } + }) + : []; + + const ret: Record> = {}; + + if (!roles || roles.includes(RoleId.OrgAdmin)) { + admins.forEach((u) => { + ret[u.UserId] = new Set([RoleId.OrgAdmin]); + }); + } + if (!roles || roles.includes(RoleId.Author)) { + project.Authors.forEach((u) => { + if (u.UserId in ret) { + ret[u.UserId].add(RoleId.Author); + } else { + ret[u.UserId] = new Set([RoleId.Author]); + } + }); + } + if (!roles || roles.includes(RoleId.AppBuilder)) { + if (project.OwnerId in ret) { + ret[project.OwnerId].add(RoleId.AppBuilder); + } else { + ret[project.OwnerId] = new Set([RoleId.AppBuilder]); + } + } + return ret; +} diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/WorkflowInstances.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/WorkflowInstances.ts new file mode 100644 index 0000000000..96dbf178b3 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/WorkflowInstances.ts @@ -0,0 +1,107 @@ +import type { Prisma } from '@prisma/client'; +import prisma from '../prisma.js'; +import { update as projectUpdate } from './Projects.js'; +import type { RequirePrimitive } from './utility.js'; + +export async function upsert( + productId: string, + instanceData: { + create: Omit, 'ProductId'>; + update: Omit, 'ProductId'>; + } +) { + const timestamp = new Date(); + const res = await prisma.workflowInstances.upsert({ + where: { + ProductId: productId + }, + create: { + ...instanceData.create, + // don't overwrite ProductId + ProductId: productId + }, + update: { + ...instanceData.update, + // don't overwrite ProductId + ProductId: productId + } + }); + + if (res.DateCreated && res.DateCreated > timestamp) { + const product = await prisma.products.findUniqueOrThrow({ + where: { + Id: productId + }, + select: { + ProjectId: true + } + }); + + await projectUpdate(product.ProjectId, { DateActive: new Date() }); + } + return res; +} + +export async function update( + productId: string, + data: Omit, 'ProductId'> +) { + return await prisma.workflowInstances.update({ + where: { + ProductId: productId + }, + // don't overwrite ProductId + data: { ...data, ProductId: productId } + }); +} + +function deleteInstance(productId: string, projectId: number) { + updateProjectDateActive(productId, projectId); + return prisma.workflowInstances.deleteMany({ where: { ProductId: productId } }); +} +export { deleteInstance as delete }; + +async function updateProjectDateActive(productId: string, projectId: number) { + const project = await prisma.projects.findUniqueOrThrow({ + where: { + Id: projectId + }, + select: { + Products: { + where: { + Id: { not: productId } + }, + select: { + WorkflowInstance: { + select: { + Id: true + } + }, + DateUpdated: true + } + }, + DateActive: true + } + }); + + const projectDateActive = project.DateActive; + + let dateActive = new Date(0); + project.Products.forEach((product) => { + if (product.WorkflowInstance) { + if (product.DateUpdated && product.DateUpdated > dateActive) { + dateActive = product.DateUpdated; + } + } + }); + + if (dateActive > new Date(0)) { + project.DateActive = dateActive; + } else { + project.DateActive = null; + } + + if (project.DateActive != projectDateActive) { + await projectUpdate(projectId, { DateActive: project.DateActive }); + } +} diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/index.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/index.ts new file mode 100644 index 0000000000..187a361825 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/index.ts @@ -0,0 +1,60 @@ +import { Prisma, PrismaClient } from '@prisma/client'; + +import prisma from '../prisma.js'; +import { WRITE_METHODS } from '../ReadonlyPrisma.js'; +import * as groupMemberships from './GroupMemberships.js'; +import * as groups from './Groups.js'; +import * as organizationMemberships from './OrganizationMemberships.js'; +import * as organizationProductDefinitions from './OrganizationProductDefinitions.js'; +import * as organizationStores from './OrganizationStores.js'; +import * as products from './Products.js'; +import * as projects from './Projects.js'; +import * as userRoles from './UserRoles.js'; +import * as utility from './utility.js'; +import * as workflowInstances from './WorkflowInstances.js'; + +type RecurseRemove = { + [K in keyof T]: T[K] extends V | null | undefined ? never : RecurseRemove; +}; + +type RemoveNested< + T extends InstanceType[Uncapitalize] +> = { + [K in keyof T]: K extends (typeof WRITE_METHODS)[number] + ? ( + args: RecurseRemove[0], { connect?: unknown; create?: unknown }> + ) => ReturnType + : T[K]; +}; + +type DataType = { + [K in keyof typeof Prisma.ModelName as Uncapitalize]: /*DefaultProxy<*/ RemoveNested< + InstanceType[Uncapitalize] + >; +}; + +const handlers = { + products, + projects, + groups, + groupMemberships, + organizationStores, + organizationProductDefinitions, + organizationMemberships, + userRoles, + utility, + workflowInstances +}; +// @ts-expect-error this is in fact immediately populated +const obj: DataType = {}; +for (const prop in Prisma.ModelName) { + const uncapitalized = prop[0].toLowerCase() + prop.substring(1); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + obj[uncapitalized] = prisma[uncapitalized]; +} + +export default { + ...obj, + ...handlers +} as const; diff --git a/source/SIL.AppBuilder.Portal/common/databaseProxy/utility.ts b/source/SIL.AppBuilder.Portal/common/databaseProxy/utility.ts new file mode 100644 index 0000000000..f025336d05 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/databaseProxy/utility.ts @@ -0,0 +1,37 @@ +import prisma from '../prisma.js'; + +export type RequirePrimitive = { + [K in keyof T]: Extract; +}; + +export async function getUserIfExists(externalId: string) { + return await prisma.users.findFirst({ + where: { + ExternalId: externalId + } + }); +} +export async function createUser(profile: { + sub?: string | null; + email?: string | null; + family_name?: string | null; + given_name?: string | null; + name?: string | null; +}) { + const result = await prisma.users.findFirst({ + where: { + ExternalId: profile.sub + } + }); + if (result) return result; + return await prisma.users.create({ + data: { + ExternalId: profile.sub, + Email: profile.email, + FamilyName: profile.family_name, + GivenName: profile.given_name, + Name: profile.name, + IsLocked: false + } + }); +} diff --git a/source/SIL.AppBuilder.Portal/common/index.ts b/source/SIL.AppBuilder.Portal/common/index.ts new file mode 100644 index 0000000000..1e1ce7cedf --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/index.ts @@ -0,0 +1,7 @@ +export * as BuildEngine from './build-engine-api/index.js'; +export * as Queues from './bullmq/queues.js'; +export * as BullMQ from './bullmq/types.js'; +export { default as DatabaseWrites } from './databaseProxy/index.js'; +export { readonlyPrisma as prisma } from './prisma.js'; +export { Workflow } from './workflow/index.js'; + diff --git a/source/SIL.AppBuilder.Portal/common/package-lock.json b/source/SIL.AppBuilder.Portal/common/package-lock.json new file mode 100644 index 0000000000..06112e1131 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/package-lock.json @@ -0,0 +1,399 @@ +{ + "name": "sil.appbuilder.portal.common", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sil.appbuilder.portal.common", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.18.0", + "bullmq": "^5.12.2", + "xstate": "^5.18.2" + }, + "devDependencies": { + "prisma": "^5.18.0" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@prisma/client": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.18.0.tgz", + "integrity": "sha512-BWivkLh+af1kqC89zCJYkHsRcyWsM8/JHpsDMM76DjP3ZdEquJhXa4IeX+HkWPnwJ5FanxEJFZZDTWiDs/Kvyw==", + "hasInstallScript": true, + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.18.0.tgz", + "integrity": "sha512-f+ZvpTLidSo3LMJxQPVgAxdAjzv5OpzAo/eF8qZqbwvgi2F5cTOI9XCpdRzJYA0iGfajjwjOKKrVq64vkxEfUw==", + "devOptional": true + }, + "node_modules/@prisma/engines": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.18.0.tgz", + "integrity": "sha512-ofmpGLeJ2q2P0wa/XaEgTnX/IsLnvSp/gZts0zjgLNdBhfuj2lowOOPmDcfKljLQUXMvAek3lw5T01kHmCG8rg==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.18.0", + "@prisma/engines-version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", + "@prisma/fetch-engine": "5.18.0", + "@prisma/get-platform": "5.18.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169.tgz", + "integrity": "sha512-a/+LpJj8vYU3nmtkg+N3X51ddbt35yYrRe8wqHTJtYQt7l1f8kjIBcCs6sHJvodW/EK5XGvboOiwm47fmNrbgg==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.18.0.tgz", + "integrity": "sha512-I/3u0x2n31rGaAuBRx2YK4eB7R/1zCuayo2DGwSpGyrJWsZesrV7QVw7ND0/Suxeo/vLkJ5OwuBqHoCxvTHpOg==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.18.0", + "@prisma/engines-version": "5.18.0-25.4c784e32044a8a016d99474bd02a3b6123742169", + "@prisma/get-platform": "5.18.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.18.0.tgz", + "integrity": "sha512-Tk+m7+uhqcKDgnMnFN0lRiH7Ewea0OEsZZs9pqXa7i3+7svS3FSCqDBCaM9x5fmhhkufiG0BtunJVDka+46DlA==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.18.0" + } + }, + "node_modules/bullmq": { + "version": "5.12.2", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.12.2.tgz", + "integrity": "sha512-DyaXf30hyqlJFVXZ/PeDLk0HTyYlS8JmeSG/y2Tlj694524VgkcwkiOPk5DsDGeEtZO7L7fTKvsepxPJ4t1p5w==", + "dependencies": { + "cron-parser": "^4.6.0", + "ioredis": "^5.4.1", + "msgpackr": "^1.10.1", + "node-abort-controller": "^3.1.1", + "semver": "^7.5.4", + "tslib": "^2.0.0", + "uuid": "^9.0.0" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ioredis": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", + "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/msgpackr": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.0.tgz", + "integrity": "sha512-I8qXuuALqJe5laEBYoFykChhSXLikZmUhccjGsPuSJ/7uPip2TJ7lwdIQwWSAi0jGZDXv4WOP8Qg65QZRuXxXw==", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/prisma": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.18.0.tgz", + "integrity": "sha512-+TrSIxZsh64OPOmaSgVPH7ALL9dfU0jceYaMJXsNrTkFHO7/3RANi5K2ZiPB1De9+KDxCWn7jvRq8y8pvk+o9g==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/engines": "5.18.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/xstate": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.18.2.tgz", + "integrity": "sha512-hab5VOe29D0agy8/7dH1lGw+7kilRQyXwpaChoMu4fe6rDP+nsHYhDYKfS2O4iXE7myA98TW6qMEudj/8NXEkA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + } + } +} diff --git a/source/SIL.AppBuilder.Portal/common/package.json b/source/SIL.AppBuilder.Portal/common/package.json new file mode 100644 index 0000000000..7fb7a814f0 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/package.json @@ -0,0 +1,22 @@ +{ + "name": "sil.appbuilder.portal.common", + "version": "1.0.0", + "description": "", + "exports": { + ".": "./index.js", + "./prisma": "./public/prisma.js", + "./workflow": "./public/workflow.js", + "./utils": "./public/utils.js" + }, + "author": "", + "type": "module", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.18.0", + "bullmq": "^5.12.2", + "xstate": "^5.18.2" + }, + "devDependencies": { + "prisma": "^5.18.0" + } +} diff --git a/source/SIL.AppBuilder.Portal/common/prisma.ts b/source/SIL.AppBuilder.Portal/common/prisma.ts new file mode 100644 index 0000000000..cc89574f01 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma.ts @@ -0,0 +1,24 @@ +import { PrismaClient } from '@prisma/client'; +import { ReadonlyClient } from './ReadonlyPrisma.js'; + +// This is the home of all database operations through prisma +// It is used from both the node-server package (which runs tasks) and from the sveltekit +// app itself. The goal is that prisma write operations should never be allowed directly +// from another package, but should instead utilize operator methods from here, which will +// handle "Form" verifications before performing writes. If implemented correctly, all +// these writes should be successful anyway, but we want to guarantee that at runtime. +// Therefore, we create a read-only version of the prisma client that can be passed out to +// other packages, but we keep the writable client behind the abstraction layer. + +if (!process.env.VITE_DATABASE_URL) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore This is necessary for browser, where import.meta.env will in fact exist + process.env.VITE_DATABASE_URL = import.meta.env.VITE_DATABASE_URL; + +const prisma = new PrismaClient(); + +export type PrismaClientExact = PrismaClient; + +export const readonlyPrisma = prisma.$extends(ReadonlyClient); + +export default prisma; diff --git a/source/SIL.AppBuilder.Portal/common/prisma/migrations/0_init/migration.sql b/source/SIL.AppBuilder.Portal/common/prisma/migrations/0_init/migration.sql new file mode 100644 index 0000000000..32d53637e1 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/migrations/0_init/migration.sql @@ -0,0 +1,1059 @@ +-- CreateExtension +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- CreateTable +CREATE TABLE "ApplicationTypes" ( + "Id" SERIAL NOT NULL, + "Name" TEXT, + "Description" TEXT, + + CONSTRAINT "PK_ApplicationTypes" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "Authors" ( + "Id" SERIAL NOT NULL, + "UserId" INTEGER NOT NULL, + "ProjectId" INTEGER NOT NULL, + "CanUpdate" BOOLEAN, + + CONSTRAINT "PK_Authors" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "Emails" ( + "Id" SERIAL NOT NULL, + "To" TEXT, + "Cc" TEXT, + "Bcc" TEXT, + "Subject" TEXT, + "ContentTemplate" TEXT, + "ContentModelJson" TEXT, + "Created" TIMESTAMP NOT NULL, + + CONSTRAINT "PK_Emails" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "GroupMemberships" ( + "Id" SERIAL NOT NULL, + "UserId" INTEGER NOT NULL, + "GroupId" INTEGER NOT NULL, + + CONSTRAINT "PK_GroupMemberships" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "Groups" ( + "Id" SERIAL NOT NULL, + "Name" TEXT, + "Abbreviation" TEXT, + "OwnerId" INTEGER NOT NULL, + + CONSTRAINT "PK_Groups" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "Notifications" ( + "Id" SERIAL NOT NULL, + "MessageId" TEXT, + "UserId" INTEGER NOT NULL, + "DateRead" TIMESTAMP, + "DateEmailSent" TIMESTAMP, + "DateCreated" TIMESTAMP, + "DateUpdated" TIMESTAMP, + "Message" TEXT, + "MessageSubstitutionsJson" TEXT, + "SendEmail" BOOLEAN NOT NULL, + "LinkUrl" TEXT, + + CONSTRAINT "PK_Notifications" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "OrganizationInviteRequests" ( + "Id" SERIAL NOT NULL, + "Name" TEXT, + "OrgAdminEmail" TEXT, + "WebsiteUrl" TEXT, + "DateCreated" TIMESTAMP, + "DateUpdated" TIMESTAMP, + + CONSTRAINT "PK_OrganizationInviteRequests" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "OrganizationInvites" ( + "Id" SERIAL NOT NULL, + "Name" TEXT, + "OwnerEmail" TEXT, + "Token" TEXT, + + CONSTRAINT "PK_OrganizationInvites" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "OrganizationMembershipInvites" ( + "Id" SERIAL NOT NULL, + "Token" UUID NOT NULL DEFAULT uuid_generate_v4(), + "Email" TEXT NOT NULL, + "Expires" TIMESTAMP NOT NULL DEFAULT (CURRENT_DATE + 7), + "Redeemed" BOOLEAN NOT NULL DEFAULT false, + "InvitedById" INTEGER NOT NULL, + "OrganizationId" INTEGER NOT NULL, + "DateCreated" TIMESTAMP, + "DateUpdated" TIMESTAMP, + + CONSTRAINT "PK_OrganizationMembershipInvites" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "OrganizationMemberships" ( + "Id" SERIAL NOT NULL, + "UserId" INTEGER NOT NULL, + "OrganizationId" INTEGER NOT NULL, + + CONSTRAINT "PK_OrganizationMemberships" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "OrganizationProductDefinitions" ( + "Id" SERIAL NOT NULL, + "OrganizationId" INTEGER NOT NULL, + "ProductDefinitionId" INTEGER NOT NULL, + + CONSTRAINT "PK_OrganizationProductDefinitions" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "OrganizationStores" ( + "Id" SERIAL NOT NULL, + "OrganizationId" INTEGER NOT NULL, + "StoreId" INTEGER NOT NULL, + + CONSTRAINT "PK_OrganizationStores" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "Organizations" ( + "Id" SERIAL NOT NULL, + "Name" TEXT, + "WebsiteUrl" TEXT, + "BuildEngineUrl" TEXT, + "BuildEngineApiAccessToken" TEXT, + "LogoUrl" TEXT, + "UseDefaultBuildEngine" BOOLEAN DEFAULT true, + "PublicByDefault" BOOLEAN DEFAULT true, + "OwnerId" INTEGER NOT NULL, + + CONSTRAINT "PK_Organizations" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "ProductArtifacts" ( + "Id" SERIAL NOT NULL, + "ProductId" UUID NOT NULL, + "ProductBuildId" INTEGER NOT NULL, + "ArtifactType" TEXT, + "Url" TEXT, + "FileSize" BIGINT, + "ContentType" TEXT, + "DateCreated" TIMESTAMP, + "DateUpdated" TIMESTAMP, + + CONSTRAINT "PK_ProductArtifacts" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "ProductBuilds" ( + "Id" SERIAL NOT NULL, + "ProductId" UUID NOT NULL, + "BuildId" INTEGER NOT NULL, + "Version" TEXT, + "DateCreated" TIMESTAMP, + "DateUpdated" TIMESTAMP, + "Success" BOOLEAN, + + CONSTRAINT "PK_ProductBuilds" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "ProductDefinitions" ( + "Id" SERIAL NOT NULL, + "Name" TEXT, + "TypeId" INTEGER NOT NULL, + "Description" TEXT, + "WorkflowId" INTEGER NOT NULL, + "RebuildWorkflowId" INTEGER, + "RepublishWorkflowId" INTEGER, + "Properties" TEXT, + + CONSTRAINT "PK_ProductDefinitions" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "ProductPublications" ( + "Id" SERIAL NOT NULL, + "ProductId" UUID NOT NULL, + "ProductBuildId" INTEGER NOT NULL, + "ReleaseId" INTEGER NOT NULL, + "Channel" TEXT, + "LogUrl" TEXT, + "Success" BOOLEAN, + "DateCreated" TIMESTAMP, + "DateUpdated" TIMESTAMP, + "Package" TEXT, + + CONSTRAINT "PK_ProductPublications" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "ProductTransitions" ( + "Id" SERIAL NOT NULL, + "ProductId" UUID NOT NULL, + "WorkflowUserId" UUID, + "AllowedUserNames" TEXT, + "InitialState" TEXT, + "DestinationState" TEXT, + "Command" TEXT, + "DateTransition" TIMESTAMP, + "Comment" TEXT, + "TransitionType" INTEGER NOT NULL DEFAULT 1, + "WorkflowType" INTEGER, + + CONSTRAINT "PK_ProductTransitions" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "Products" ( + "Id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "ProjectId" INTEGER NOT NULL, + "ProductDefinitionId" INTEGER NOT NULL, + "StoreId" INTEGER, + "StoreLanguageId" INTEGER, + "DateCreated" TIMESTAMP, + "DateUpdated" TIMESTAMP, + "WorkflowJobId" INTEGER NOT NULL, + "WorkflowBuildId" INTEGER NOT NULL, + "DateBuilt" TIMESTAMP, + "WorkflowPublishId" INTEGER NOT NULL, + "WorkflowComment" TEXT, + "DatePublished" TIMESTAMP, + "PublishLink" TEXT, + "VersionBuilt" TEXT, + "Properties" TEXT, + + CONSTRAINT "PK_Products" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "ProjectImports" ( + "Id" SERIAL NOT NULL, + "ImportData" TEXT, + "TypeId" INTEGER, + "OwnerId" INTEGER, + "GroupId" INTEGER, + "OrganizationId" INTEGER, + "DateCreated" TIMESTAMP, + "DateUpdated" TIMESTAMP, + + CONSTRAINT "PK_ProjectImports" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "Projects" ( + "Id" SERIAL NOT NULL, + "Name" TEXT, + "TypeId" INTEGER NOT NULL, + "Description" TEXT, + "OwnerId" INTEGER NOT NULL, + "GroupId" INTEGER NOT NULL, + "OrganizationId" INTEGER NOT NULL, + "Language" TEXT, + "IsPublic" BOOLEAN DEFAULT true, + "DateCreated" TIMESTAMP, + "DateUpdated" TIMESTAMP, + "DateArchived" TIMESTAMP, + "AllowDownloads" BOOLEAN DEFAULT true, + "AutomaticBuilds" BOOLEAN DEFAULT true, + "WorkflowProjectId" INTEGER NOT NULL DEFAULT 0, + "WorkflowProjectUrl" TEXT, + "WorkflowAppProjectUrl" TEXT, + "DateActive" TIMESTAMP, + "ImportId" INTEGER, + + CONSTRAINT "PK_Projects" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "Reviewers" ( + "Id" SERIAL NOT NULL, + "Name" TEXT, + "Email" TEXT, + "ProjectId" INTEGER NOT NULL, + "Locale" TEXT, + + CONSTRAINT "PK_Reviewers" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "Roles" ( + "Id" SERIAL NOT NULL, + "RoleName" INTEGER NOT NULL, + + CONSTRAINT "PK_Roles" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "StoreLanguages" ( + "Id" SERIAL NOT NULL, + "Name" TEXT, + "Description" TEXT, + "StoreTypeId" INTEGER NOT NULL, + + CONSTRAINT "PK_StoreLanguages" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "StoreTypes" ( + "Id" SERIAL NOT NULL, + "Name" TEXT, + "Description" TEXT, + + CONSTRAINT "PK_StoreTypes" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "Stores" ( + "Id" SERIAL NOT NULL, + "Name" TEXT, + "Description" TEXT, + "StoreTypeId" INTEGER NOT NULL, + + CONSTRAINT "PK_Stores" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "SystemStatuses" ( + "Id" SERIAL NOT NULL, + "BuildEngineUrl" TEXT, + "BuildEngineApiAccessToken" TEXT, + "SystemAvailable" BOOLEAN NOT NULL, + "DateCreated" TIMESTAMP, + "DateUpdated" TIMESTAMP, + + CONSTRAINT "PK_SystemStatuses" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "UserRoles" ( + "Id" SERIAL NOT NULL, + "UserId" INTEGER NOT NULL, + "RoleId" INTEGER NOT NULL, + "OrganizationId" INTEGER NOT NULL, + + CONSTRAINT "PK_UserRoles" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "UserTasks" ( + "Id" SERIAL NOT NULL, + "UserId" INTEGER NOT NULL, + "ProductId" UUID NOT NULL, + "ActivityName" TEXT, + "Status" TEXT, + "Comment" TEXT, + "DateCreated" TIMESTAMP, + "DateUpdated" TIMESTAMP, + + CONSTRAINT "PK_UserTasks" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "Users" ( + "Id" SERIAL NOT NULL, + "Name" TEXT, + "GivenName" TEXT, + "FamilyName" TEXT, + "Email" TEXT, + "Phone" TEXT, + "Timezone" TEXT, + "Locale" TEXT, + "IsLocked" BOOLEAN NOT NULL, + "ExternalId" TEXT, + "ProfileVisibility" INTEGER NOT NULL DEFAULT 1, + "EmailNotification" BOOLEAN DEFAULT true, + "WorkflowUserId" UUID, + "DateCreated" TIMESTAMP, + "DateUpdated" TIMESTAMP, + + CONSTRAINT "PK_Users" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "WorkflowDefinitions" ( + "Id" SERIAL NOT NULL, + "Name" TEXT, + "Enabled" BOOLEAN NOT NULL, + "Description" TEXT, + "WorkflowScheme" TEXT, + "WorkflowBusinessFlow" TEXT, + "StoreTypeId" INTEGER, + "Type" INTEGER NOT NULL DEFAULT 1, + "Properties" TEXT, + + CONSTRAINT "PK_WorkflowDefinitions" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "WorkflowGlobalParameter" ( + "Id" UUID NOT NULL, + "Type" VARCHAR(512) NOT NULL, + "Name" VARCHAR(256) NOT NULL, + "Value" TEXT NOT NULL, + + CONSTRAINT "WorkflowGlobalParameter_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "WorkflowInbox" ( + "Id" UUID NOT NULL, + "ProcessId" UUID NOT NULL, + "IdentityId" VARCHAR(256) NOT NULL, + + CONSTRAINT "WorkflowInbox_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "WorkflowProcessInstance" ( + "Id" UUID NOT NULL, + "StateName" VARCHAR(256), + "ActivityName" VARCHAR(256) NOT NULL, + "SchemeId" UUID NOT NULL, + "PreviousState" VARCHAR(256), + "PreviousStateForDirect" VARCHAR(256), + "PreviousStateForReverse" VARCHAR(256), + "PreviousActivity" VARCHAR(256), + "PreviousActivityForDirect" VARCHAR(256), + "PreviousActivityForReverse" VARCHAR(256), + "IsDeterminingParametersChanged" BOOLEAN NOT NULL, + "ParentProcessId" UUID, + "RootProcessId" UUID NOT NULL, + + CONSTRAINT "WorkflowProcessInstance_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "WorkflowProcessInstancePersistence" ( + "Id" UUID NOT NULL, + "ProcessId" UUID NOT NULL, + "ParameterName" VARCHAR(256) NOT NULL, + "Value" TEXT NOT NULL, + + CONSTRAINT "WorkflowProcessInstancePersistence_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "WorkflowProcessInstanceStatus" ( + "Id" UUID NOT NULL, + "Status" SMALLINT NOT NULL, + "Lock" UUID NOT NULL, + + CONSTRAINT "WorkflowProcessInstanceStatus_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "WorkflowProcessScheme" ( + "Id" UUID NOT NULL, + "Scheme" TEXT NOT NULL, + "DefiningParameters" TEXT NOT NULL, + "DefiningParametersHash" VARCHAR(24) NOT NULL, + "SchemeCode" VARCHAR(256) NOT NULL, + "IsObsolete" BOOLEAN NOT NULL, + "RootSchemeCode" VARCHAR(256), + "RootSchemeId" UUID, + "AllowedActivities" TEXT, + "StartingTransition" TEXT, + + CONSTRAINT "WorkflowProcessScheme_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "WorkflowProcessTimer" ( + "Id" UUID NOT NULL, + "ProcessId" UUID NOT NULL, + "Name" VARCHAR(256) NOT NULL, + "NextExecutionDateTime" TIMESTAMP(6) NOT NULL, + "Ignore" BOOLEAN NOT NULL, + + CONSTRAINT "WorkflowProcessTimer_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "WorkflowProcessTransitionHistory" ( + "Id" UUID NOT NULL, + "ProcessId" UUID NOT NULL, + "ExecutorIdentityId" VARCHAR(256), + "ActorIdentityId" VARCHAR(256), + "FromActivityName" VARCHAR(256) NOT NULL, + "ToActivityName" VARCHAR(256) NOT NULL, + "ToStateName" VARCHAR(256), + "TransitionTime" TIMESTAMP(6) NOT NULL, + "TransitionClassifier" VARCHAR(256) NOT NULL, + "FromStateName" VARCHAR(256), + "TriggerName" VARCHAR(256), + "IsFinalised" BOOLEAN NOT NULL, + + CONSTRAINT "WorkflowProcessTransitionHistory_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "WorkflowScheme" ( + "Code" VARCHAR(256) NOT NULL, + "Scheme" TEXT NOT NULL, + "CanBeInlined" BOOLEAN NOT NULL DEFAULT false, + "InlinedSchemes" VARCHAR(1024), + + CONSTRAINT "WorkflowScheme_pkey" PRIMARY KEY ("Code") +); + +-- CreateTable +CREATE TABLE "__EFMigrationsHistory" ( + "MigrationId" VARCHAR(150) NOT NULL, + "ProductVersion" VARCHAR(32) NOT NULL, + + CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") +); + +-- CreateTable +CREATE TABLE "dwAppSettings" ( + "Name" VARCHAR(50) NOT NULL, + "Value" VARCHAR(1000) NOT NULL, + "GroupName" VARCHAR(50), + "ParamName" VARCHAR(1024) NOT NULL, + "Order" INTEGER, + "EditorType" VARCHAR(50) NOT NULL DEFAULT 0, + "IsHidden" BOOLEAN NOT NULL DEFAULT (0)::boolean, + + CONSTRAINT "dwAppSettings_pkey" PRIMARY KEY ("Name") +); + +-- CreateTable +CREATE TABLE "dwSecurityCredential" ( + "Id" UUID NOT NULL, + "PasswordHash" VARCHAR(128), + "PasswordSalt" VARCHAR(128), + "SecurityUserId" UUID NOT NULL, + "Login" VARCHAR(256) NOT NULL, + "AuthenticationType" SMALLINT NOT NULL, + + CONSTRAINT "dwSecurityCredential_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "dwSecurityGroup" ( + "Id" UUID NOT NULL, + "Name" VARCHAR(128) NOT NULL, + "Comment" VARCHAR(1000), + "IsSyncWithDomainGroup" BOOLEAN NOT NULL DEFAULT (0)::boolean, + + CONSTRAINT "dwSecurityGroup_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "dwSecurityGroupToSecurityRole" ( + "Id" UUID NOT NULL, + "SecurityRoleId" UUID NOT NULL, + "SecurityGroupId" UUID NOT NULL, + + CONSTRAINT "dwSecurityGroupToSecurityRole_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "dwSecurityGroupToSecurityUser" ( + "Id" UUID NOT NULL, + "SecurityUserId" UUID NOT NULL, + "SecurityGroupId" UUID NOT NULL, + + CONSTRAINT "dwSecurityGroupToSecurityUser_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "dwSecurityPermission" ( + "Id" UUID NOT NULL, + "Code" VARCHAR(128) NOT NULL, + "Name" VARCHAR(128), + "GroupId" UUID NOT NULL, + + CONSTRAINT "dwSecurityPermission_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "dwSecurityPermissionGroup" ( + "Id" UUID NOT NULL, + "Name" VARCHAR(128) NOT NULL, + "Code" VARCHAR(128) NOT NULL, + + CONSTRAINT "dwSecurityPermissionGroup_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "dwSecurityRole" ( + "Id" UUID NOT NULL, + "Code" VARCHAR(128) NOT NULL, + "Name" VARCHAR(128) NOT NULL, + "Comment" VARCHAR(1000), + "DomainGroup" VARCHAR(512), + + CONSTRAINT "dwSecurityRole_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "dwSecurityRoleToSecurityPermission" ( + "Id" UUID NOT NULL, + "SecurityRoleId" UUID NOT NULL, + "SecurityPermissionId" UUID NOT NULL, + "AccessType" SMALLINT NOT NULL DEFAULT 0, + + CONSTRAINT "dwSecurityRoleToSecurityPermission_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "dwSecurityUser" ( + "Id" UUID NOT NULL, + "Name" VARCHAR(256) NOT NULL, + "Email" VARCHAR(256), + "IsLocked" BOOLEAN NOT NULL DEFAULT (0)::boolean, + "ExternalId" VARCHAR(1024), + "Timezone" VARCHAR(256), + "Localization" VARCHAR(256), + "DecimalSeparator" CHAR(1), + "PageSize" INTEGER, + "StartPage" VARCHAR(256), + "IsRTL" BOOLEAN DEFAULT (0)::boolean, + + CONSTRAINT "dwSecurityUser_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "dwSecurityUserImpersonation" ( + "Id" UUID NOT NULL, + "SecurityUserId" UUID NOT NULL, + "ImpSecurityUserId" UUID NOT NULL, + "DateFrom" TIMESTAMP(6) NOT NULL, + "DateTo" TIMESTAMP(6) NOT NULL, + + CONSTRAINT "dwSecurityUserImpersonation_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "dwSecurityUserState" ( + "Id" UUID NOT NULL, + "SecurityUserId" UUID NOT NULL, + "Key" VARCHAR(256) NOT NULL, + "Value" TEXT NOT NULL, + + CONSTRAINT "dwSecurityUserState_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "dwSecurityUserToSecurityRole" ( + "Id" UUID NOT NULL, + "SecurityRoleId" UUID NOT NULL, + "SecurityUserId" UUID NOT NULL, + + CONSTRAINT "dwSecurityUserToSecurityRole_pkey" PRIMARY KEY ("Id") +); + +-- CreateTable +CREATE TABLE "dwUploadedFiles" ( + "Id" UUID NOT NULL, + "Data" BYTEA NOT NULL, + "AttachmentLength" BIGINT NOT NULL, + "Used" BOOLEAN NOT NULL DEFAULT (0)::boolean, + "Name" VARCHAR(1000) NOT NULL, + "ContentType" VARCHAR(255) NOT NULL, + "CreatedBy" VARCHAR(1024), + "CreatedDate" TIMESTAMP(6), + "UpdatedBy" VARCHAR(1024), + "UpdatedDate" TIMESTAMP(6), + "Properties" TEXT, + + CONSTRAINT "dwUploadedFiles_pkey" PRIMARY KEY ("Id") +); + +-- CreateIndex +CREATE INDEX "IX_Authors_ProjectId" ON "Authors"("ProjectId"); + +-- CreateIndex +CREATE INDEX "IX_Authors_UserId" ON "Authors"("UserId"); + +-- CreateIndex +CREATE INDEX "IX_GroupMemberships_GroupId" ON "GroupMemberships"("GroupId"); + +-- CreateIndex +CREATE INDEX "IX_GroupMemberships_UserId" ON "GroupMemberships"("UserId"); + +-- CreateIndex +CREATE INDEX "IX_Groups_OwnerId" ON "Groups"("OwnerId"); + +-- CreateIndex +CREATE INDEX "IX_Notifications_UserId" ON "Notifications"("UserId"); + +-- CreateIndex +CREATE INDEX "IX_OrganizationMembershipInvites_InvitedById" ON "OrganizationMembershipInvites"("InvitedById"); + +-- CreateIndex +CREATE INDEX "IX_OrganizationMembershipInvites_OrganizationId" ON "OrganizationMembershipInvites"("OrganizationId"); + +-- CreateIndex +CREATE INDEX "IX_OrganizationMemberships_OrganizationId" ON "OrganizationMemberships"("OrganizationId"); + +-- CreateIndex +CREATE INDEX "IX_OrganizationMemberships_UserId" ON "OrganizationMemberships"("UserId"); + +-- CreateIndex +CREATE INDEX "IX_OrganizationProductDefinitions_OrganizationId" ON "OrganizationProductDefinitions"("OrganizationId"); + +-- CreateIndex +CREATE INDEX "IX_OrganizationProductDefinitions_ProductDefinitionId" ON "OrganizationProductDefinitions"("ProductDefinitionId"); + +-- CreateIndex +CREATE INDEX "IX_OrganizationStores_OrganizationId" ON "OrganizationStores"("OrganizationId"); + +-- CreateIndex +CREATE INDEX "IX_OrganizationStores_StoreId" ON "OrganizationStores"("StoreId"); + +-- CreateIndex +CREATE INDEX "IX_Organizations_OwnerId" ON "Organizations"("OwnerId"); + +-- CreateIndex +CREATE INDEX "IX_ProductArtifacts_ProductBuildId" ON "ProductArtifacts"("ProductBuildId"); + +-- CreateIndex +CREATE INDEX "IX_ProductArtifacts_ProductId" ON "ProductArtifacts"("ProductId"); + +-- CreateIndex +CREATE INDEX "IX_ProductBuilds_ProductId" ON "ProductBuilds"("ProductId"); + +-- CreateIndex +CREATE INDEX "IX_ProductDefinitions_RebuildWorkflowId" ON "ProductDefinitions"("RebuildWorkflowId"); + +-- CreateIndex +CREATE INDEX "IX_ProductDefinitions_RepublishWorkflowId" ON "ProductDefinitions"("RepublishWorkflowId"); + +-- CreateIndex +CREATE INDEX "IX_ProductDefinitions_TypeId" ON "ProductDefinitions"("TypeId"); + +-- CreateIndex +CREATE INDEX "IX_ProductDefinitions_WorkflowId" ON "ProductDefinitions"("WorkflowId"); + +-- CreateIndex +CREATE INDEX "IX_ProductPublications_Package" ON "ProductPublications"("Package"); + +-- CreateIndex +CREATE INDEX "IX_ProductPublications_ProductBuildId" ON "ProductPublications"("ProductBuildId"); + +-- CreateIndex +CREATE INDEX "IX_ProductPublications_ProductId" ON "ProductPublications"("ProductId"); + +-- CreateIndex +CREATE INDEX "IX_ProductTransitions_ProductId" ON "ProductTransitions"("ProductId"); + +-- CreateIndex +CREATE INDEX "IX_Products_ProductDefinitionId" ON "Products"("ProductDefinitionId"); + +-- CreateIndex +CREATE INDEX "IX_Products_ProjectId" ON "Products"("ProjectId"); + +-- CreateIndex +CREATE INDEX "IX_Products_StoreId" ON "Products"("StoreId"); + +-- CreateIndex +CREATE INDEX "IX_Products_StoreLanguageId" ON "Products"("StoreLanguageId"); + +-- CreateIndex +CREATE INDEX "IX_ProjectImports_GroupId" ON "ProjectImports"("GroupId"); + +-- CreateIndex +CREATE INDEX "IX_ProjectImports_OrganizationId" ON "ProjectImports"("OrganizationId"); + +-- CreateIndex +CREATE INDEX "IX_ProjectImports_OwnerId" ON "ProjectImports"("OwnerId"); + +-- CreateIndex +CREATE INDEX "IX_ProjectImports_TypeId" ON "ProjectImports"("TypeId"); + +-- CreateIndex +CREATE INDEX "IX_Projects_GroupId" ON "Projects"("GroupId"); + +-- CreateIndex +CREATE INDEX "IX_Projects_ImportId" ON "Projects"("ImportId"); + +-- CreateIndex +CREATE INDEX "IX_Projects_OrganizationId" ON "Projects"("OrganizationId"); + +-- CreateIndex +CREATE INDEX "IX_Projects_OwnerId" ON "Projects"("OwnerId"); + +-- CreateIndex +CREATE INDEX "IX_Projects_TypeId" ON "Projects"("TypeId"); + +-- CreateIndex +CREATE INDEX "IX_Reviewers_ProjectId" ON "Reviewers"("ProjectId"); + +-- CreateIndex +CREATE INDEX "IX_StoreLanguages_StoreTypeId" ON "StoreLanguages"("StoreTypeId"); + +-- CreateIndex +CREATE INDEX "IX_Stores_StoreTypeId" ON "Stores"("StoreTypeId"); + +-- CreateIndex +CREATE INDEX "IX_UserRoles_OrganizationId" ON "UserRoles"("OrganizationId"); + +-- CreateIndex +CREATE INDEX "IX_UserRoles_RoleId" ON "UserRoles"("RoleId"); + +-- CreateIndex +CREATE INDEX "IX_UserRoles_UserId" ON "UserRoles"("UserId"); + +-- CreateIndex +CREATE INDEX "IX_UserTasks_ProductId" ON "UserTasks"("ProductId"); + +-- CreateIndex +CREATE INDEX "IX_UserTasks_UserId" ON "UserTasks"("UserId"); + +-- CreateIndex +CREATE INDEX "IX_Users_WorkflowUserId" ON "Users"("WorkflowUserId"); + +-- CreateIndex +CREATE INDEX "IX_WorkflowDefinitions_StoreTypeId" ON "WorkflowDefinitions"("StoreTypeId"); + +-- CreateIndex +CREATE INDEX "WorkflowGlobalParameter_Name_idx" ON "WorkflowGlobalParameter"("Name"); + +-- CreateIndex +CREATE INDEX "WorkflowGlobalParameter_Type_idx" ON "WorkflowGlobalParameter"("Type"); + +-- CreateIndex +CREATE INDEX "WorkflowInbox_IdentityId_idx" ON "WorkflowInbox"("IdentityId"); + +-- CreateIndex +CREATE INDEX "WorkflowInbox_ProcessId_idx" ON "WorkflowInbox"("ProcessId"); + +-- CreateIndex +CREATE INDEX "WorkflowProcessInstancePersistence_ProcessId_idx" ON "WorkflowProcessInstancePersistence"("ProcessId"); + +-- CreateIndex +CREATE INDEX "WorkflowProcessInstanceStatus_Status_idx" ON "WorkflowProcessInstanceStatus"("Status"); + +-- CreateIndex +CREATE INDEX "WorkflowProcessScheme_DefiningParametersHash_idx" ON "WorkflowProcessScheme"("DefiningParametersHash"); + +-- CreateIndex +CREATE INDEX "WorkflowProcessScheme_IsObsolete_idx" ON "WorkflowProcessScheme"("IsObsolete"); + +-- CreateIndex +CREATE INDEX "WorkflowProcessScheme_SchemeCode_idx" ON "WorkflowProcessScheme"("SchemeCode"); + +-- CreateIndex +CREATE INDEX "WorkflowProcessTimer_Ignore_idx" ON "WorkflowProcessTimer"("Ignore"); + +-- CreateIndex +CREATE INDEX "WorkflowProcessTimer_Name_idx" ON "WorkflowProcessTimer"("Name"); + +-- CreateIndex +CREATE INDEX "WorkflowProcessTimer_NextExecutionDateTime_idx" ON "WorkflowProcessTimer"("NextExecutionDateTime"); + +-- CreateIndex +CREATE INDEX "WorkflowProcessTimer_ProcessId_idx" ON "WorkflowProcessTimer"("ProcessId"); + +-- CreateIndex +CREATE INDEX "WorkflowProcessTransitionHistory_ActorIdentityId_idx" ON "WorkflowProcessTransitionHistory"("ActorIdentityId"); + +-- CreateIndex +CREATE INDEX "WorkflowProcessTransitionHistory_ExecutorIdentityId_idx" ON "WorkflowProcessTransitionHistory"("ExecutorIdentityId"); + +-- CreateIndex +CREATE INDEX "WorkflowProcessTransitionHistory_ProcessId_idx" ON "WorkflowProcessTransitionHistory"("ProcessId"); + +-- AddForeignKey +ALTER TABLE "Authors" ADD CONSTRAINT "FK_Authors_Projects_ProjectId" FOREIGN KEY ("ProjectId") REFERENCES "Projects"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Authors" ADD CONSTRAINT "FK_Authors_Users_UserId" FOREIGN KEY ("UserId") REFERENCES "Users"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "GroupMemberships" ADD CONSTRAINT "FK_GroupMemberships_Groups_GroupId" FOREIGN KEY ("GroupId") REFERENCES "Groups"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "GroupMemberships" ADD CONSTRAINT "FK_GroupMemberships_Users_UserId" FOREIGN KEY ("UserId") REFERENCES "Users"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Groups" ADD CONSTRAINT "FK_Groups_Organizations_OwnerId" FOREIGN KEY ("OwnerId") REFERENCES "Organizations"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Notifications" ADD CONSTRAINT "FK_Notifications_Users_UserId" FOREIGN KEY ("UserId") REFERENCES "Users"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "OrganizationMembershipInvites" ADD CONSTRAINT "FK_OrganizationMembershipInvites_Organizations_OrganizationId" FOREIGN KEY ("OrganizationId") REFERENCES "Organizations"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "OrganizationMembershipInvites" ADD CONSTRAINT "FK_OrganizationMembershipInvites_Users_InvitedById" FOREIGN KEY ("InvitedById") REFERENCES "Users"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "OrganizationMemberships" ADD CONSTRAINT "FK_OrganizationMemberships_Organizations_OrganizationId" FOREIGN KEY ("OrganizationId") REFERENCES "Organizations"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "OrganizationMemberships" ADD CONSTRAINT "FK_OrganizationMemberships_Users_UserId" FOREIGN KEY ("UserId") REFERENCES "Users"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "OrganizationProductDefinitions" ADD CONSTRAINT "FK_OrganizationProductDefinitions_Organizations_OrganizationId" FOREIGN KEY ("OrganizationId") REFERENCES "Organizations"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "OrganizationProductDefinitions" ADD CONSTRAINT "FK_OrganizationProductDefinitions_ProductDefinitions_ProductDe~" FOREIGN KEY ("ProductDefinitionId") REFERENCES "ProductDefinitions"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "OrganizationStores" ADD CONSTRAINT "FK_OrganizationStores_Organizations_OrganizationId" FOREIGN KEY ("OrganizationId") REFERENCES "Organizations"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "OrganizationStores" ADD CONSTRAINT "FK_OrganizationStores_Stores_StoreId" FOREIGN KEY ("StoreId") REFERENCES "Stores"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Organizations" ADD CONSTRAINT "FK_Organizations_Users_OwnerId" FOREIGN KEY ("OwnerId") REFERENCES "Users"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProductArtifacts" ADD CONSTRAINT "FK_ProductArtifacts_ProductBuilds_ProductBuildId" FOREIGN KEY ("ProductBuildId") REFERENCES "ProductBuilds"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProductArtifacts" ADD CONSTRAINT "FK_ProductArtifacts_Products_ProductId" FOREIGN KEY ("ProductId") REFERENCES "Products"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProductBuilds" ADD CONSTRAINT "FK_ProductBuilds_Products_ProductId" FOREIGN KEY ("ProductId") REFERENCES "Products"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProductDefinitions" ADD CONSTRAINT "FK_ProductDefinitions_ApplicationTypes_TypeId" FOREIGN KEY ("TypeId") REFERENCES "ApplicationTypes"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProductDefinitions" ADD CONSTRAINT "FK_ProductDefinitions_WorkflowDefinitions_RebuildWorkflowId" FOREIGN KEY ("RebuildWorkflowId") REFERENCES "WorkflowDefinitions"("Id") ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProductDefinitions" ADD CONSTRAINT "FK_ProductDefinitions_WorkflowDefinitions_RepublishWorkflowId" FOREIGN KEY ("RepublishWorkflowId") REFERENCES "WorkflowDefinitions"("Id") ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProductDefinitions" ADD CONSTRAINT "FK_ProductDefinitions_WorkflowDefinitions_WorkflowId" FOREIGN KEY ("WorkflowId") REFERENCES "WorkflowDefinitions"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProductPublications" ADD CONSTRAINT "FK_ProductPublications_ProductBuilds_ProductBuildId" FOREIGN KEY ("ProductBuildId") REFERENCES "ProductBuilds"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProductPublications" ADD CONSTRAINT "FK_ProductPublications_Products_ProductId" FOREIGN KEY ("ProductId") REFERENCES "Products"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProductTransitions" ADD CONSTRAINT "FK_ProductTransitions_Products_ProductId" FOREIGN KEY ("ProductId") REFERENCES "Products"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Products" ADD CONSTRAINT "FK_Products_ProductDefinitions_ProductDefinitionId" FOREIGN KEY ("ProductDefinitionId") REFERENCES "ProductDefinitions"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Products" ADD CONSTRAINT "FK_Products_Projects_ProjectId" FOREIGN KEY ("ProjectId") REFERENCES "Projects"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Products" ADD CONSTRAINT "FK_Products_StoreLanguages_StoreLanguageId" FOREIGN KEY ("StoreLanguageId") REFERENCES "StoreLanguages"("Id") ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Products" ADD CONSTRAINT "FK_Products_Stores_StoreId" FOREIGN KEY ("StoreId") REFERENCES "Stores"("Id") ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProjectImports" ADD CONSTRAINT "FK_ProjectImports_ApplicationTypes_TypeId" FOREIGN KEY ("TypeId") REFERENCES "ApplicationTypes"("Id") ON DELETE SET NULL ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProjectImports" ADD CONSTRAINT "FK_ProjectImports_Groups_GroupId" FOREIGN KEY ("GroupId") REFERENCES "Groups"("Id") ON DELETE SET NULL ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProjectImports" ADD CONSTRAINT "FK_ProjectImports_Organizations_OrganizationId" FOREIGN KEY ("OrganizationId") REFERENCES "Organizations"("Id") ON DELETE SET NULL ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "ProjectImports" ADD CONSTRAINT "FK_ProjectImports_Users_OwnerId" FOREIGN KEY ("OwnerId") REFERENCES "Users"("Id") ON DELETE SET NULL ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Projects" ADD CONSTRAINT "FK_Projects_ApplicationTypes_TypeId" FOREIGN KEY ("TypeId") REFERENCES "ApplicationTypes"("Id") ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Projects" ADD CONSTRAINT "FK_Projects_Groups_GroupId" FOREIGN KEY ("GroupId") REFERENCES "Groups"("Id") ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Projects" ADD CONSTRAINT "FK_Projects_Organizations_OrganizationId" FOREIGN KEY ("OrganizationId") REFERENCES "Organizations"("Id") ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Projects" ADD CONSTRAINT "FK_Projects_ProjectImports_ImportId" FOREIGN KEY ("ImportId") REFERENCES "ProjectImports"("Id") ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Projects" ADD CONSTRAINT "FK_Projects_Users_OwnerId" FOREIGN KEY ("OwnerId") REFERENCES "Users"("Id") ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Reviewers" ADD CONSTRAINT "FK_Reviewers_Projects_ProjectId" FOREIGN KEY ("ProjectId") REFERENCES "Projects"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "StoreLanguages" ADD CONSTRAINT "FK_StoreLanguages_StoreTypes_StoreTypeId" FOREIGN KEY ("StoreTypeId") REFERENCES "StoreTypes"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "Stores" ADD CONSTRAINT "FK_Stores_StoreTypes_StoreTypeId" FOREIGN KEY ("StoreTypeId") REFERENCES "StoreTypes"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "UserRoles" ADD CONSTRAINT "FK_UserRoles_Organizations_OrganizationId" FOREIGN KEY ("OrganizationId") REFERENCES "Organizations"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "UserRoles" ADD CONSTRAINT "FK_UserRoles_Roles_RoleId" FOREIGN KEY ("RoleId") REFERENCES "Roles"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "UserRoles" ADD CONSTRAINT "FK_UserRoles_Users_UserId" FOREIGN KEY ("UserId") REFERENCES "Users"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "UserTasks" ADD CONSTRAINT "FK_UserTasks_Products_ProductId" FOREIGN KEY ("ProductId") REFERENCES "Products"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "UserTasks" ADD CONSTRAINT "FK_UserTasks_Users_UserId" FOREIGN KEY ("UserId") REFERENCES "Users"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "WorkflowDefinitions" ADD CONSTRAINT "FK_WorkflowDefinitions_StoreTypes_StoreTypeId" FOREIGN KEY ("StoreTypeId") REFERENCES "StoreTypes"("Id") ON DELETE RESTRICT ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "dwSecurityCredential" ADD CONSTRAINT "dwSecurityCredential_SecurityUserId_fkey" FOREIGN KEY ("SecurityUserId") REFERENCES "dwSecurityUser"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "dwSecurityGroupToSecurityRole" ADD CONSTRAINT "dwSecurityGroupToSecurityRole_SecurityGroupId_fkey" FOREIGN KEY ("SecurityGroupId") REFERENCES "dwSecurityGroup"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "dwSecurityGroupToSecurityRole" ADD CONSTRAINT "dwSecurityGroupToSecurityRole_SecurityRoleId_fkey" FOREIGN KEY ("SecurityRoleId") REFERENCES "dwSecurityRole"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "dwSecurityGroupToSecurityUser" ADD CONSTRAINT "dwSecurityGroupToSecurityUser_SecurityGroupId_fkey" FOREIGN KEY ("SecurityGroupId") REFERENCES "dwSecurityGroup"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "dwSecurityGroupToSecurityUser" ADD CONSTRAINT "dwSecurityGroupToSecurityUser_SecurityUserId_fkey" FOREIGN KEY ("SecurityUserId") REFERENCES "dwSecurityUser"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "dwSecurityPermission" ADD CONSTRAINT "dwSecurityPermission_GroupId_fkey" FOREIGN KEY ("GroupId") REFERENCES "dwSecurityPermissionGroup"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "dwSecurityRoleToSecurityPermission" ADD CONSTRAINT "dwSecurityRoleToSecurityPermission_SecurityPermissionId_fkey" FOREIGN KEY ("SecurityPermissionId") REFERENCES "dwSecurityPermission"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "dwSecurityRoleToSecurityPermission" ADD CONSTRAINT "dwSecurityRoleToSecurityPermission_SecurityRoleId_fkey" FOREIGN KEY ("SecurityRoleId") REFERENCES "dwSecurityRole"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "dwSecurityUserImpersonation" ADD CONSTRAINT "dwSecurityUserImpersonation_ImpSecurityUserId_fkey" FOREIGN KEY ("ImpSecurityUserId") REFERENCES "dwSecurityUser"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "dwSecurityUserImpersonation" ADD CONSTRAINT "dwSecurityUserImpersonation_SecurityUserId_fkey" FOREIGN KEY ("SecurityUserId") REFERENCES "dwSecurityUser"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "dwSecurityUserState" ADD CONSTRAINT "dwSecurityUserState_SecurityUserId_fkey" FOREIGN KEY ("SecurityUserId") REFERENCES "dwSecurityUser"("Id") ON DELETE CASCADE ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "dwSecurityUserToSecurityRole" ADD CONSTRAINT "dwSecurityUserToSecurityRole_SecurityRoleId_fkey" FOREIGN KEY ("SecurityRoleId") REFERENCES "dwSecurityRole"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- AddForeignKey +ALTER TABLE "dwSecurityUserToSecurityRole" ADD CONSTRAINT "dwSecurityUserToSecurityRole_SecurityUserId_fkey" FOREIGN KEY ("SecurityUserId") REFERENCES "dwSecurityUser"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION; + diff --git a/source/SIL.AppBuilder.Portal/common/prisma/migrations/1_workflow_instances/migration.sql b/source/SIL.AppBuilder.Portal/common/prisma/migrations/1_workflow_instances/migration.sql new file mode 100644 index 0000000000..647c5beb5a --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/migrations/1_workflow_instances/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "WorkflowInstances" ( + "Id" SERIAL NOT NULL, + "Snapshot" TEXT NOT NULL, + "ProductId" UUID NOT NULL, + + CONSTRAINT "PK_WorkflowInstances" PRIMARY KEY ("Id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "WorkflowInstances_ProductId_key" ON "WorkflowInstances"("ProductId"); + +-- AddForeignKey +ALTER TABLE "WorkflowInstances" ADD CONSTRAINT "FK_WorkflowInstances_Products_ProductId" FOREIGN KEY ("ProductId") REFERENCES "Products"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION; diff --git a/source/SIL.AppBuilder.Portal/common/prisma/migrations/2_prisma_auto_date/migration.sql b/source/SIL.AppBuilder.Portal/common/prisma/migrations/2_prisma_auto_date/migration.sql new file mode 100644 index 0000000000..f48faa77fa --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/migrations/2_prisma_auto_date/migration.sql @@ -0,0 +1,35 @@ +-- AlterTable +ALTER TABLE "Notifications" ALTER COLUMN "DateCreated" SET DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "OrganizationInviteRequests" ALTER COLUMN "DateCreated" SET DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "OrganizationMembershipInvites" ALTER COLUMN "DateCreated" SET DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "ProductArtifacts" ALTER COLUMN "DateCreated" SET DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "ProductBuilds" ALTER COLUMN "DateCreated" SET DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "ProductPublications" ALTER COLUMN "DateCreated" SET DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "Products" ALTER COLUMN "DateCreated" SET DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "ProjectImports" ALTER COLUMN "DateCreated" SET DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "Projects" ALTER COLUMN "DateCreated" SET DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "SystemStatuses" ALTER COLUMN "DateCreated" SET DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "UserTasks" ALTER COLUMN "DateCreated" SET DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "Users" ALTER COLUMN "DateCreated" SET DEFAULT CURRENT_TIMESTAMP; diff --git a/source/SIL.AppBuilder.Portal/common/prisma/migrations/3_workflow_config/migration.sql b/source/SIL.AppBuilder.Portal/common/prisma/migrations/3_workflow_config/migration.sql new file mode 100644 index 0000000000..267ed5cfea --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/migrations/3_workflow_config/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "WorkflowDefinitions" ADD COLUMN "AdminRequirements" INTEGER[] DEFAULT ARRAY[0]::INTEGER[], +ADD COLUMN "ProductType" INTEGER NOT NULL DEFAULT 0; diff --git a/source/SIL.AppBuilder.Portal/common/prisma/migrations/4_workflow_instances_refactor/migration.sql b/source/SIL.AppBuilder.Portal/common/prisma/migrations/4_workflow_instances_refactor/migration.sql new file mode 100644 index 0000000000..96df438256 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/migrations/4_workflow_instances_refactor/migration.sql @@ -0,0 +1,13 @@ +-- AlterTable +ALTER TABLE "WorkflowInstances" DROP COLUMN "Snapshot", +ADD COLUMN "Context" TEXT NOT NULL, +ADD COLUMN "State" TEXT NOT NULL, +ADD COLUMN "WorkflowDefinitionId" INTEGER NOT NULL, +ADD COLUMN "DateCreated" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "DateUpdated" TIMESTAMP; + +-- AddForeignKey +ALTER TABLE "WorkflowInstances" ADD CONSTRAINT "FK_WorkflowInstances_WorkflowDefinitions_WorkflowDefinitionId" FOREIGN KEY ("WorkflowDefinitionId") REFERENCES "WorkflowDefinitions"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION; + +-- DropIndex +DROP INDEX "WorkflowInstances_ProductId_key"; diff --git a/source/SIL.AppBuilder.Portal/common/prisma/migrations/5_workflow_config_refactor/migration.sql b/source/SIL.AppBuilder.Portal/common/prisma/migrations/5_workflow_config_refactor/migration.sql new file mode 100644 index 0000000000..947c2457bc --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/migrations/5_workflow_config_refactor/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "WorkflowDefinitions" DROP COLUMN "AdminRequirements", +ADD COLUMN "WorkflowOptions" INTEGER[] DEFAULT ARRAY[]::INTEGER[]; diff --git a/source/SIL.AppBuilder.Portal/common/prisma/migrations/6_products_instances_1-1/migration.sql b/source/SIL.AppBuilder.Portal/common/prisma/migrations/6_products_instances_1-1/migration.sql new file mode 100644 index 0000000000..578090de91 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/migrations/6_products_instances_1-1/migration.sql @@ -0,0 +1,2 @@ +-- CreateIndex +CREATE UNIQUE INDEX "WorkflowInstances_ProductId_key" ON "WorkflowInstances"("ProductId"); diff --git a/source/SIL.AppBuilder.Portal/common/prisma/migrations/7_org_invites/migration.sql b/source/SIL.AppBuilder.Portal/common/prisma/migrations/7_org_invites/migration.sql new file mode 100644 index 0000000000..c8671f4893 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/migrations/7_org_invites/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "OrganizationMembershipInvites" +ADD "Roles" INTEGER[] DEFAULT ARRAY[]::INTEGER[], +ADD "Groups" INTEGER[] DEFAULT ARRAY[]::INTEGER[]; diff --git a/source/SIL.AppBuilder.Portal/common/prisma/migrations/8_task_roles/migration.sql b/source/SIL.AppBuilder.Portal/common/prisma/migrations/8_task_roles/migration.sql new file mode 100644 index 0000000000..565d50b5ba --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/migrations/8_task_roles/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "UserTasks" ADD COLUMN "Role" INTEGER NOT NULL DEFAULT 3; diff --git a/source/SIL.AppBuilder.Portal/common/prisma/migrations/migration_lock.toml b/source/SIL.AppBuilder.Portal/common/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000000..fbffa92c2b --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/common/prisma/schema.prisma b/source/SIL.AppBuilder.Portal/common/prisma/schema.prisma new file mode 100644 index 0000000000..f734d68924 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/schema.prisma @@ -0,0 +1,770 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["postgresqlExtensions"] +} + +datasource db { + provider = "postgresql" + url = env("VITE_DATABASE_URL") + extensions = [uuidOssp(map: "uuid-ossp")] +} + +model ApplicationTypes { + Id Int @id(map: "PK_ApplicationTypes") @default(autoincrement()) + Name String? + Description String? + ProductDefinitions ProductDefinitions[] + ProjectImports ProjectImports[] + Projects Projects[] +} + +model Authors { + Id Int @id(map: "PK_Authors") @default(autoincrement()) + UserId Int + ProjectId Int + CanUpdate Boolean? + Projects Projects @relation(fields: [ProjectId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_Authors_Projects_ProjectId") + Users Users @relation(fields: [UserId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_Authors_Users_UserId") + + @@index([ProjectId], map: "IX_Authors_ProjectId") + @@index([UserId], map: "IX_Authors_UserId") +} + +model Emails { + Id Int @id(map: "PK_Emails") @default(autoincrement()) + To String? + Cc String? + Bcc String? + Subject String? + ContentTemplate String? + ContentModelJson String? + Created DateTime @db.Timestamp +} + +model GroupMemberships { + Id Int @id(map: "PK_GroupMemberships") @default(autoincrement()) + UserId Int + GroupId Int + Group Groups @relation(fields: [GroupId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_GroupMemberships_Groups_GroupId") + User Users @relation(fields: [UserId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_GroupMemberships_Users_UserId") + + @@index([GroupId], map: "IX_GroupMemberships_GroupId") + @@index([UserId], map: "IX_GroupMemberships_UserId") +} + +model Groups { + Id Int @id(map: "PK_Groups") @default(autoincrement()) + Name String? + Abbreviation String? + OwnerId Int + GroupMemberships GroupMemberships[] + Owner Organizations @relation(fields: [OwnerId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_Groups_Organizations_OwnerId") + ProjectImports ProjectImports[] + Projects Projects[] + + @@index([OwnerId], map: "IX_Groups_OwnerId") +} + +model Notifications { + Id Int @id(map: "PK_Notifications") @default(autoincrement()) + MessageId String? + UserId Int + DateRead DateTime? @db.Timestamp + DateEmailSent DateTime? @db.Timestamp + DateCreated DateTime? @default(now()) @db.Timestamp + DateUpdated DateTime? @updatedAt @db.Timestamp + Message String? + MessageSubstitutionsJson String? + SendEmail Boolean + LinkUrl String? + User Users @relation(fields: [UserId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_Notifications_Users_UserId") + + @@index([UserId], map: "IX_Notifications_UserId") +} + +model OrganizationInviteRequests { + Id Int @id(map: "PK_OrganizationInviteRequests") @default(autoincrement()) + Name String? + OrgAdminEmail String? + WebsiteUrl String? + DateCreated DateTime? @default(now()) @db.Timestamp + DateUpdated DateTime? @updatedAt @db.Timestamp +} + +model OrganizationInvites { + Id Int @id(map: "PK_OrganizationInvites") @default(autoincrement()) + Name String? + OwnerEmail String? + Token String? +} + +model OrganizationMembershipInvites { + Id Int @id(map: "PK_OrganizationMembershipInvites") @default(autoincrement()) + Token String @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + Email String + Expires DateTime @default(dbgenerated("(CURRENT_DATE + 7)")) @db.Timestamp + Redeemed Boolean @default(false) + InvitedById Int + OrganizationId Int + Roles Int[] @default([]) + Groups Int[] @default([]) + DateCreated DateTime? @default(now()) @db.Timestamp + DateUpdated DateTime? @updatedAt @db.Timestamp + Organization Organizations @relation(fields: [OrganizationId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_OrganizationMembershipInvites_Organizations_OrganizationId") + User Users @relation(fields: [InvitedById], references: [Id], onDelete: NoAction, onUpdate: NoAction, map: "FK_OrganizationMembershipInvites_Users_InvitedById") + + @@index([InvitedById], map: "IX_OrganizationMembershipInvites_InvitedById") + @@index([OrganizationId], map: "IX_OrganizationMembershipInvites_OrganizationId") +} + +model OrganizationMemberships { + Id Int @id(map: "PK_OrganizationMemberships") @default(autoincrement()) + UserId Int + OrganizationId Int + Organization Organizations @relation(fields: [OrganizationId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_OrganizationMemberships_Organizations_OrganizationId") + User Users @relation(fields: [UserId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_OrganizationMemberships_Users_UserId") + + @@index([OrganizationId], map: "IX_OrganizationMemberships_OrganizationId") + @@index([UserId], map: "IX_OrganizationMemberships_UserId") +} + +model OrganizationProductDefinitions { + Id Int @id(map: "PK_OrganizationProductDefinitions") @default(autoincrement()) + OrganizationId Int + ProductDefinitionId Int + Organization Organizations @relation(fields: [OrganizationId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_OrganizationProductDefinitions_Organizations_OrganizationId") + ProductDefinition ProductDefinitions @relation(fields: [ProductDefinitionId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_OrganizationProductDefinitions_ProductDefinitions_ProductDe~") + + @@index([OrganizationId], map: "IX_OrganizationProductDefinitions_OrganizationId") + @@index([ProductDefinitionId], map: "IX_OrganizationProductDefinitions_ProductDefinitionId") +} + +model OrganizationStores { + Id Int @id(map: "PK_OrganizationStores") @default(autoincrement()) + OrganizationId Int + StoreId Int + Organization Organizations @relation(fields: [OrganizationId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_OrganizationStores_Organizations_OrganizationId") + Store Stores @relation(fields: [StoreId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_OrganizationStores_Stores_StoreId") + + @@index([OrganizationId], map: "IX_OrganizationStores_OrganizationId") + @@index([StoreId], map: "IX_OrganizationStores_StoreId") +} + +model Organizations { + Id Int @id(map: "PK_Organizations") @default(autoincrement()) + Name String? + WebsiteUrl String? + BuildEngineUrl String? + BuildEngineApiAccessToken String? + LogoUrl String? + UseDefaultBuildEngine Boolean? @default(true) + PublicByDefault Boolean? @default(true) + OwnerId Int + Groups Groups[] + OrganizationMembershipInvites OrganizationMembershipInvites[] + OrganizationMemberships OrganizationMemberships[] + OrganizationProductDefinitions OrganizationProductDefinitions[] + OrganizationStores OrganizationStores[] + Owner Users @relation(fields: [OwnerId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_Organizations_Users_OwnerId") + ProjectImports ProjectImports[] + Projects Projects[] + UserRoles UserRoles[] + + @@index([OwnerId], map: "IX_Organizations_OwnerId") +} + +model ProductArtifacts { + Id Int @id(map: "PK_ProductArtifacts") @default(autoincrement()) + ProductId String @db.Uuid + ProductBuildId Int + ArtifactType String? + Url String? + FileSize BigInt? + ContentType String? + DateCreated DateTime? @default(now()) @db.Timestamp + DateUpdated DateTime? @updatedAt @db.Timestamp + ProductBuild ProductBuilds @relation(fields: [ProductBuildId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_ProductArtifacts_ProductBuilds_ProductBuildId") + Product Products @relation(fields: [ProductId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_ProductArtifacts_Products_ProductId") + + @@index([ProductBuildId], map: "IX_ProductArtifacts_ProductBuildId") + @@index([ProductId], map: "IX_ProductArtifacts_ProductId") +} + +model ProductBuilds { + Id Int @id(map: "PK_ProductBuilds") @default(autoincrement()) + ProductId String @db.Uuid + BuildId Int + Version String? + DateCreated DateTime? @default(now()) @db.Timestamp + DateUpdated DateTime? @updatedAt @db.Timestamp + Success Boolean? + ProductArtifacts ProductArtifacts[] + Product Products @relation(fields: [ProductId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_ProductBuilds_Products_ProductId") + ProductPublications ProductPublications[] + + @@index([ProductId], map: "IX_ProductBuilds_ProductId") +} + +model ProductDefinitions { + Id Int @id(map: "PK_ProductDefinitions") @default(autoincrement()) + Name String? + TypeId Int + Description String? + WorkflowId Int + RebuildWorkflowId Int? + RepublishWorkflowId Int? + Properties String? + OrganizationProductDefinitions OrganizationProductDefinitions[] + ApplicationTypes ApplicationTypes @relation(fields: [TypeId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_ProductDefinitions_ApplicationTypes_TypeId") + RebuildWorkflow WorkflowDefinitions? @relation("ProductDefinitions_RebuildWorkflowIdToWorkflowDefinitions", fields: [RebuildWorkflowId], references: [Id], onDelete: Restrict, onUpdate: NoAction, map: "FK_ProductDefinitions_WorkflowDefinitions_RebuildWorkflowId") + RepublishWorkflow WorkflowDefinitions? @relation("ProductDefinitions_RepublishWorkflowIdToWorkflowDefinitions", fields: [RepublishWorkflowId], references: [Id], onDelete: Restrict, onUpdate: NoAction, map: "FK_ProductDefinitions_WorkflowDefinitions_RepublishWorkflowId") + Workflow WorkflowDefinitions @relation("ProductDefinitions_WorkflowIdToWorkflowDefinitions", fields: [WorkflowId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_ProductDefinitions_WorkflowDefinitions_WorkflowId") + Products Products[] + + @@index([RebuildWorkflowId], map: "IX_ProductDefinitions_RebuildWorkflowId") + @@index([RepublishWorkflowId], map: "IX_ProductDefinitions_RepublishWorkflowId") + @@index([TypeId], map: "IX_ProductDefinitions_TypeId") + @@index([WorkflowId], map: "IX_ProductDefinitions_WorkflowId") +} + +model ProductPublications { + Id Int @id(map: "PK_ProductPublications") @default(autoincrement()) + ProductId String @db.Uuid + ProductBuildId Int + ReleaseId Int + Channel String? + LogUrl String? + Success Boolean? + DateCreated DateTime? @default(now()) @db.Timestamp + DateUpdated DateTime? @updatedAt @db.Timestamp + Package String? + ProductBuild ProductBuilds @relation(fields: [ProductBuildId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_ProductPublications_ProductBuilds_ProductBuildId") + Product Products @relation(fields: [ProductId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_ProductPublications_Products_ProductId") + + @@index([Package], map: "IX_ProductPublications_Package") + @@index([ProductBuildId], map: "IX_ProductPublications_ProductBuildId") + @@index([ProductId], map: "IX_ProductPublications_ProductId") +} + +model ProductTransitions { + Id Int @id(map: "PK_ProductTransitions") @default(autoincrement()) + ProductId String @db.Uuid + // TODO: remove when DWKit is replaced + // Could possibly be replaced with regular UserId, but probably shouldn't, because what if a User is deleted? + // Maybe replace with name? Or perhaps this would already be taken care of by populating AllowedUserNames with just the name of the user who caused the product transition... + WorkflowUserId String? @db.Uuid + AllowedUserNames String? + InitialState String? + DestinationState String? + Command String? + DateTransition DateTime? @db.Timestamp + Comment String? + TransitionType Int @default(1) + WorkflowType Int? + Product Products @relation(fields: [ProductId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_ProductTransitions_Products_ProductId") + + @@index([ProductId], map: "IX_ProductTransitions_ProductId") +} + +model Products { + Id String @id(map: "PK_Products") @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + ProjectId Int + ProductDefinitionId Int + StoreId Int? + StoreLanguageId Int? + DateCreated DateTime? @default(now()) @db.Timestamp + DateUpdated DateTime? @updatedAt @db.Timestamp + WorkflowJobId Int + WorkflowBuildId Int + DateBuilt DateTime? @db.Timestamp + WorkflowPublishId Int + WorkflowComment String? + DatePublished DateTime? @db.Timestamp + PublishLink String? + VersionBuilt String? + Properties String? + ProductArtifacts ProductArtifacts[] + ProductBuilds ProductBuilds[] + ProductPublications ProductPublications[] + ProductTransitions ProductTransitions[] + ProductDefinition ProductDefinitions @relation(fields: [ProductDefinitionId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_Products_ProductDefinitions_ProductDefinitionId") + Project Projects @relation(fields: [ProjectId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_Products_Projects_ProjectId") + StoreLanguage StoreLanguages? @relation(fields: [StoreLanguageId], references: [Id], onDelete: Restrict, onUpdate: NoAction, map: "FK_Products_StoreLanguages_StoreLanguageId") + Store Stores? @relation(fields: [StoreId], references: [Id], onDelete: Restrict, onUpdate: NoAction, map: "FK_Products_Stores_StoreId") + UserTasks UserTasks[] + WorkflowInstance WorkflowInstances? + + @@index([ProductDefinitionId], map: "IX_Products_ProductDefinitionId") + @@index([ProjectId], map: "IX_Products_ProjectId") + @@index([StoreId], map: "IX_Products_StoreId") + @@index([StoreLanguageId], map: "IX_Products_StoreLanguageId") +} + +model ProjectImports { + Id Int @id(map: "PK_ProjectImports") @default(autoincrement()) + ImportData String? + TypeId Int? + OwnerId Int? + GroupId Int? + OrganizationId Int? + DateCreated DateTime? @default(now()) @db.Timestamp + DateUpdated DateTime? @updatedAt @db.Timestamp + ApplicationTypes ApplicationTypes? @relation(fields: [TypeId], references: [Id], onUpdate: NoAction, map: "FK_ProjectImports_ApplicationTypes_TypeId") + Groups Groups? @relation(fields: [GroupId], references: [Id], onUpdate: NoAction, map: "FK_ProjectImports_Groups_GroupId") + Organizations Organizations? @relation(fields: [OrganizationId], references: [Id], onUpdate: NoAction, map: "FK_ProjectImports_Organizations_OrganizationId") + Users Users? @relation(fields: [OwnerId], references: [Id], onUpdate: NoAction, map: "FK_ProjectImports_Users_OwnerId") + Projects Projects[] + + @@index([GroupId], map: "IX_ProjectImports_GroupId") + @@index([OrganizationId], map: "IX_ProjectImports_OrganizationId") + @@index([OwnerId], map: "IX_ProjectImports_OwnerId") + @@index([TypeId], map: "IX_ProjectImports_TypeId") +} + +model Projects { + Id Int @id(map: "PK_Projects") @default(autoincrement()) + Name String? + TypeId Int + Description String? + OwnerId Int + GroupId Int + OrganizationId Int + Language String? + IsPublic Boolean? @default(true) + DateCreated DateTime? @default(now()) @db.Timestamp + DateUpdated DateTime? @updatedAt @db.Timestamp + DateArchived DateTime? @db.Timestamp + AllowDownloads Boolean? @default(true) + AutomaticBuilds Boolean? @default(true) + WorkflowProjectId Int @default(0) + WorkflowProjectUrl String? + WorkflowAppProjectUrl String? + DateActive DateTime? @db.Timestamp + ImportId Int? + Authors Authors[] + Products Products[] + ApplicationType ApplicationTypes @relation(fields: [TypeId], references: [Id], onUpdate: NoAction, map: "FK_Projects_ApplicationTypes_TypeId") + Group Groups @relation(fields: [GroupId], references: [Id], onUpdate: NoAction, map: "FK_Projects_Groups_GroupId") + Organization Organizations @relation(fields: [OrganizationId], references: [Id], onUpdate: NoAction, map: "FK_Projects_Organizations_OrganizationId") + ProjectImport ProjectImports? @relation(fields: [ImportId], references: [Id], onDelete: Restrict, onUpdate: NoAction, map: "FK_Projects_ProjectImports_ImportId") + Owner Users @relation(fields: [OwnerId], references: [Id], onUpdate: NoAction, map: "FK_Projects_Users_OwnerId") + Reviewers Reviewers[] + + @@index([GroupId], map: "IX_Projects_GroupId") + @@index([ImportId], map: "IX_Projects_ImportId") + @@index([OrganizationId], map: "IX_Projects_OrganizationId") + @@index([OwnerId], map: "IX_Projects_OwnerId") + @@index([TypeId], map: "IX_Projects_TypeId") +} + +model Reviewers { + Id Int @id(map: "PK_Reviewers") @default(autoincrement()) + Name String? + Email String? + ProjectId Int + Locale String? + Project Projects @relation(fields: [ProjectId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_Reviewers_Projects_ProjectId") + + @@index([ProjectId], map: "IX_Reviewers_ProjectId") +} + +model Roles { + Id Int @id(map: "PK_Roles") @default(autoincrement()) + RoleName Int + UserRoles UserRoles[] +} + +model StoreLanguages { + Id Int @id(map: "PK_StoreLanguages") @default(autoincrement()) + Name String? + Description String? + StoreTypeId Int + Products Products[] + StoreType StoreTypes @relation(fields: [StoreTypeId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_StoreLanguages_StoreTypes_StoreTypeId") + + @@index([StoreTypeId], map: "IX_StoreLanguages_StoreTypeId") +} + +model StoreTypes { + Id Int @id(map: "PK_StoreTypes") @default(autoincrement()) + Name String? + Description String? + StoreLanguages StoreLanguages[] + Stores Stores[] + WorkflowDefinitions WorkflowDefinitions[] +} + +model Stores { + Id Int @id(map: "PK_Stores") @default(autoincrement()) + Name String? + Description String? + StoreTypeId Int + OrganizationStores OrganizationStores[] + Products Products[] + StoreType StoreTypes @relation(fields: [StoreTypeId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_Stores_StoreTypes_StoreTypeId") + + @@index([StoreTypeId], map: "IX_Stores_StoreTypeId") +} + +model SystemStatuses { + Id Int @id(map: "PK_SystemStatuses") @default(autoincrement()) + BuildEngineUrl String? + BuildEngineApiAccessToken String? + SystemAvailable Boolean + DateCreated DateTime? @default(now()) @db.Timestamp + DateUpdated DateTime? @updatedAt @db.Timestamp +} + +model UserRoles { + Id Int @id(map: "PK_UserRoles") @default(autoincrement()) + UserId Int + RoleId Int + OrganizationId Int + Organization Organizations @relation(fields: [OrganizationId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_UserRoles_Organizations_OrganizationId") + Role Roles @relation(fields: [RoleId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_UserRoles_Roles_RoleId") + User Users @relation(fields: [UserId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_UserRoles_Users_UserId") + + @@index([OrganizationId], map: "IX_UserRoles_OrganizationId") + @@index([RoleId], map: "IX_UserRoles_RoleId") + @@index([UserId], map: "IX_UserRoles_UserId") +} + +model UserTasks { + Id Int @id(map: "PK_UserTasks") @default(autoincrement()) + UserId Int + ProductId String @db.Uuid + ActivityName String? + Status String? + Comment String? + DateCreated DateTime? @default(now()) @db.Timestamp + DateUpdated DateTime? @updatedAt @db.Timestamp + Role Int @default(3) // AppBuilder (i.e. the Project Owner) + Product Products @relation(fields: [ProductId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_UserTasks_Products_ProductId") + User Users @relation(fields: [UserId], references: [Id], onDelete: Cascade, onUpdate: NoAction, map: "FK_UserTasks_Users_UserId") + + @@index([ProductId], map: "IX_UserTasks_ProductId") + @@index([UserId], map: "IX_UserTasks_UserId") +} + +model Users { + Id Int @id(map: "PK_Users") @default(autoincrement()) + Name String? + GivenName String? + FamilyName String? + Email String? + Phone String? + Timezone String? + Locale String? + IsLocked Boolean + ExternalId String? + ProfileVisibility Int @default(1) + EmailNotification Boolean? @default(true) + WorkflowUserId String? @db.Uuid // TODO: remove when DWKit is replaced + DateCreated DateTime? @default(now()) @db.Timestamp + DateUpdated DateTime? @updatedAt @db.Timestamp + Authors Authors[] + GroupMemberships GroupMemberships[] + Notifications Notifications[] + OrganizationMembershipInvites OrganizationMembershipInvites[] + OrganizationMemberships OrganizationMemberships[] + Organizations Organizations[] + ProjectImports ProjectImports[] + Projects Projects[] + UserRoles UserRoles[] + UserTasks UserTasks[] + + @@index([WorkflowUserId], map: "IX_Users_WorkflowUserId") +} + +model WorkflowDefinitions { + Id Int @id(map: "PK_WorkflowDefinitions") @default(autoincrement()) + Name String? + Enabled Boolean + Description String? + WorkflowScheme String? // TODO: remove when DWKit is replaced + WorkflowBusinessFlow String? // TODO: remove when DWKit is replaced + StoreTypeId Int? + Type Int @default(1) + Properties String? + RebuildWorkflows ProductDefinitions[] @relation("ProductDefinitions_RebuildWorkflowIdToWorkflowDefinitions") + RepublishWorkflows ProductDefinitions[] @relation("ProductDefinitions_RepublishWorkflowIdToWorkflowDefinitions") + Workflows ProductDefinitions[] @relation("ProductDefinitions_WorkflowIdToWorkflowDefinitions") + StoreType StoreTypes? @relation(fields: [StoreTypeId], references: [Id], onDelete: Restrict, onUpdate: NoAction, map: "FK_WorkflowDefinitions_StoreTypes_StoreTypeId") + ProductType Int @default(0) + WorkflowOptions Int[] @default([]) + WorkflowInstances WorkflowInstances[] + + @@index([StoreTypeId], map: "IX_WorkflowDefinitions_StoreTypeId") +} + +// +// DWKIT Tables - will be removed later when DWKit is replaced +// + +model WorkflowGlobalParameter { + Id String @id @db.Uuid + Type String @db.VarChar(512) + Name String @db.VarChar(256) + Value String + + @@index([Name]) + @@index([Type]) +} + +model WorkflowInbox { + Id String @id @db.Uuid + ProcessId String @db.Uuid + IdentityId String @db.VarChar(256) + + @@index([IdentityId]) + @@index([ProcessId]) +} + +model WorkflowProcessInstance { + Id String @id @db.Uuid + StateName String? @db.VarChar(256) + ActivityName String @db.VarChar(256) + SchemeId String @db.Uuid + PreviousState String? @db.VarChar(256) + PreviousStateForDirect String? @db.VarChar(256) + PreviousStateForReverse String? @db.VarChar(256) + PreviousActivity String? @db.VarChar(256) + PreviousActivityForDirect String? @db.VarChar(256) + PreviousActivityForReverse String? @db.VarChar(256) + IsDeterminingParametersChanged Boolean + ParentProcessId String? @db.Uuid + RootProcessId String @db.Uuid +} + +model WorkflowProcessInstancePersistence { + Id String @id @db.Uuid + ProcessId String @db.Uuid + ParameterName String @db.VarChar(256) + Value String + + @@index([ProcessId]) +} + +model WorkflowProcessInstanceStatus { + Id String @id @db.Uuid + Status Int @db.SmallInt + Lock String @db.Uuid + + @@index([Status]) +} + +model WorkflowProcessScheme { + Id String @id @db.Uuid + Scheme String + DefiningParameters String + DefiningParametersHash String @db.VarChar(24) + SchemeCode String @db.VarChar(256) + IsObsolete Boolean + RootSchemeCode String? @db.VarChar(256) + RootSchemeId String? @db.Uuid + AllowedActivities String? + StartingTransition String? + + @@index([DefiningParametersHash]) + @@index([IsObsolete]) + @@index([SchemeCode]) +} + +model WorkflowProcessTimer { + Id String @id @db.Uuid + ProcessId String @db.Uuid + Name String @db.VarChar(256) + NextExecutionDateTime DateTime @db.Timestamp(6) + Ignore Boolean + + @@index([Ignore]) + @@index([Name]) + @@index([NextExecutionDateTime]) + @@index([ProcessId]) +} + +model WorkflowProcessTransitionHistory { + Id String @id @db.Uuid + ProcessId String @db.Uuid + ExecutorIdentityId String? @db.VarChar(256) + ActorIdentityId String? @db.VarChar(256) + FromActivityName String @db.VarChar(256) + ToActivityName String @db.VarChar(256) + ToStateName String? @db.VarChar(256) + TransitionTime DateTime @db.Timestamp(6) + TransitionClassifier String @db.VarChar(256) + FromStateName String? @db.VarChar(256) + TriggerName String? @db.VarChar(256) + IsFinalised Boolean + + @@index([ActorIdentityId]) + @@index([ExecutorIdentityId]) + @@index([ProcessId]) +} + +model WorkflowScheme { + Code String @id @db.VarChar(256) + Scheme String + CanBeInlined Boolean @default(false) + InlinedSchemes String? @db.VarChar(1024) +} + +model EFMigrationsHistory { + MigrationId String @id(map: "PK___EFMigrationsHistory") @db.VarChar(150) + ProductVersion String @db.VarChar(32) + + @@map("__EFMigrationsHistory") +} + +model dwAppSettings { + Name String @id @db.VarChar(50) + Value String @db.VarChar(1000) + GroupName String? @db.VarChar(50) + ParamName String @db.VarChar(1024) + Order Int? + EditorType String @default(dbgenerated("0")) @db.VarChar(50) + IsHidden Boolean @default(dbgenerated("(0)::boolean")) +} + +model dwSecurityCredential { + Id String @id @db.Uuid + PasswordHash String? @db.VarChar(128) + PasswordSalt String? @db.VarChar(128) + SecurityUserId String @db.Uuid + Login String @db.VarChar(256) + AuthenticationType Int @db.SmallInt + dwSecurityUser dwSecurityUser @relation(fields: [SecurityUserId], references: [Id], onDelete: Cascade, onUpdate: NoAction) +} + +model dwSecurityGroup { + Id String @id @db.Uuid + Name String @db.VarChar(128) + Comment String? @db.VarChar(1000) + IsSyncWithDomainGroup Boolean @default(dbgenerated("(0)::boolean")) + dwSecurityGroupToSecurityRole dwSecurityGroupToSecurityRole[] + dwSecurityGroupToSecurityUser dwSecurityGroupToSecurityUser[] +} + +model dwSecurityGroupToSecurityRole { + Id String @id @db.Uuid + SecurityRoleId String @db.Uuid + SecurityGroupId String @db.Uuid + dwSecurityGroup dwSecurityGroup @relation(fields: [SecurityGroupId], references: [Id], onDelete: NoAction, onUpdate: NoAction) + dwSecurityRole dwSecurityRole @relation(fields: [SecurityRoleId], references: [Id], onDelete: NoAction, onUpdate: NoAction) +} + +model dwSecurityGroupToSecurityUser { + Id String @id @db.Uuid + SecurityUserId String @db.Uuid + SecurityGroupId String @db.Uuid + dwSecurityGroup dwSecurityGroup @relation(fields: [SecurityGroupId], references: [Id], onDelete: NoAction, onUpdate: NoAction) + dwSecurityUser dwSecurityUser @relation(fields: [SecurityUserId], references: [Id], onDelete: NoAction, onUpdate: NoAction) +} + +model dwSecurityPermission { + Id String @id @db.Uuid + Code String @db.VarChar(128) + Name String? @db.VarChar(128) + GroupId String @db.Uuid + dwSecurityPermissionGroup dwSecurityPermissionGroup @relation(fields: [GroupId], references: [Id], onDelete: Cascade, onUpdate: NoAction) + dwSecurityRoleToSecurityPermission dwSecurityRoleToSecurityPermission[] +} + +model dwSecurityPermissionGroup { + Id String @id @db.Uuid + Name String @db.VarChar(128) + Code String @db.VarChar(128) + dwSecurityPermission dwSecurityPermission[] +} + +model dwSecurityRole { + Id String @id @db.Uuid + Code String @db.VarChar(128) + Name String @db.VarChar(128) + Comment String? @db.VarChar(1000) + DomainGroup String? @db.VarChar(512) + dwSecurityGroupToSecurityRole dwSecurityGroupToSecurityRole[] + dwSecurityRoleToSecurityPermission dwSecurityRoleToSecurityPermission[] + dwSecurityUserToSecurityRole dwSecurityUserToSecurityRole[] +} + +model dwSecurityRoleToSecurityPermission { + Id String @id @db.Uuid + SecurityRoleId String @db.Uuid + SecurityPermissionId String @db.Uuid + AccessType Int @default(0) @db.SmallInt + dwSecurityPermission dwSecurityPermission @relation(fields: [SecurityPermissionId], references: [Id], onDelete: Cascade, onUpdate: NoAction) + dwSecurityRole dwSecurityRole @relation(fields: [SecurityRoleId], references: [Id], onDelete: NoAction, onUpdate: NoAction) +} + +model dwSecurityUser { + Id String @id @db.Uuid + Name String @db.VarChar(256) + Email String? @db.VarChar(256) + IsLocked Boolean @default(dbgenerated("(0)::boolean")) + ExternalId String? @db.VarChar(1024) + Timezone String? @db.VarChar(256) + Localization String? @db.VarChar(256) + DecimalSeparator String? @db.Char(1) + PageSize Int? + StartPage String? @db.VarChar(256) + IsRTL Boolean? @default(dbgenerated("(0)::boolean")) + dwSecurityCredential dwSecurityCredential[] + dwSecurityGroupToSecurityUser dwSecurityGroupToSecurityUser[] + dwSecurityUserImpersonation_dwSecurityUserImpersonation_ImpSecurityUserIdTodwSecurityUser dwSecurityUserImpersonation[] @relation("dwSecurityUserImpersonation_ImpSecurityUserIdTodwSecurityUser") + dwSecurityUserImpersonation_dwSecurityUserImpersonation_SecurityUserIdTodwSecurityUser dwSecurityUserImpersonation[] @relation("dwSecurityUserImpersonation_SecurityUserIdTodwSecurityUser") + dwSecurityUserState dwSecurityUserState[] + dwSecurityUserToSecurityRole dwSecurityUserToSecurityRole[] +} + +model dwSecurityUserImpersonation { + Id String @id @db.Uuid + SecurityUserId String @db.Uuid + ImpSecurityUserId String @db.Uuid + DateFrom DateTime @db.Timestamp(6) + DateTo DateTime @db.Timestamp(6) + dwSecurityUser_dwSecurityUserImpersonation_ImpSecurityUserIdTodwSecurityUser dwSecurityUser @relation("dwSecurityUserImpersonation_ImpSecurityUserIdTodwSecurityUser", fields: [ImpSecurityUserId], references: [Id], onDelete: NoAction, onUpdate: NoAction) + dwSecurityUser_dwSecurityUserImpersonation_SecurityUserIdTodwSecurityUser dwSecurityUser @relation("dwSecurityUserImpersonation_SecurityUserIdTodwSecurityUser", fields: [SecurityUserId], references: [Id], onDelete: Cascade, onUpdate: NoAction) +} + +model dwSecurityUserState { + Id String @id @db.Uuid + SecurityUserId String @db.Uuid + Key String @db.VarChar(256) + Value String + dwSecurityUser dwSecurityUser @relation(fields: [SecurityUserId], references: [Id], onDelete: Cascade, onUpdate: NoAction) +} + +model dwSecurityUserToSecurityRole { + Id String @id @db.Uuid + SecurityRoleId String @db.Uuid + SecurityUserId String @db.Uuid + dwSecurityRole dwSecurityRole @relation(fields: [SecurityRoleId], references: [Id], onDelete: NoAction, onUpdate: NoAction) + dwSecurityUser dwSecurityUser @relation(fields: [SecurityUserId], references: [Id], onDelete: NoAction, onUpdate: NoAction) +} + +model dwUploadedFiles { + Id String @id @db.Uuid + Data Bytes + AttachmentLength BigInt + Used Boolean @default(dbgenerated("(0)::boolean")) + Name String @db.VarChar(1000) + ContentType String @db.VarChar(255) + CreatedBy String? @db.VarChar(1024) + CreatedDate DateTime? @db.Timestamp(6) + UpdatedBy String? @db.VarChar(1024) + UpdatedDate DateTime? @db.Timestamp(6) + Properties String? +} + +model WorkflowInstances { + Id Int @id(map: "PK_WorkflowInstances") @default(autoincrement()) + State String + Context String + WorkflowDefinitionId Int + ProductId String @unique @db.Uuid + DateCreated DateTime? @default(now()) @db.Timestamp + DateUpdated DateTime? @updatedAt @db.Timestamp + Product Products @relation(fields: [ProductId], references: [Id], onDelete: NoAction, onUpdate: NoAction, map: "FK_WorkflowInstances_Products_ProductId") + WorkflowDefinition WorkflowDefinitions @relation(fields: [WorkflowDefinitionId], references: [Id], onDelete: NoAction, onUpdate: NoAction, map: "FK_WorkflowInstances_WorkflowDefinitions_WorkflowDefinitionId") +} diff --git a/source/SIL.AppBuilder.Portal/common/prisma/seed.ts b/source/SIL.AppBuilder.Portal/common/prisma/seed.ts new file mode 100644 index 0000000000..c5f9cb37b0 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/seed.ts @@ -0,0 +1,608 @@ +import { PrismaClient } from '@prisma/client'; +import { Command, Option } from 'commander'; + +const program = new Command(); +program + .addOption(new Option('-v, --verbose [level]', 'verbose logging').argParser(parseInt).default(0)) + .addOption(new Option('-o, --organizations', 'include default organizations')); +program.parse(process.argv); +const options = program.opts(); +if (options.verbose) console.log(options); + +const prisma = new PrismaClient(); +async function main() { + type Role = [number, number]; + const roles: Role[] = [ + [1, 1], + [2, 2], + [3, 3], + [4, 4] + ]; + for (const [Id, RoleName] of roles) { + await prisma.roles.upsert({ where: { Id }, update: {}, create: { Id, RoleName } }); + } + + type ApplicationType = [number, string, string]; + const applicationTypes: ApplicationType[] = [ + [1, 'scriptureappBuilder', 'Scripture App Builder'], + [2, 'readingappbuilder', 'Reading App Builder'], + [3, 'dictionaryappbuilder', 'Dictionary App Builder'], + [4, 'keyboardappbuilder', 'Keyboard App Builder'] + ]; + for (const [Id, Name, Description] of applicationTypes) { + await prisma.applicationTypes.upsert({ + where: { Id }, + update: {}, + create: { Id, Name, Description } + }); + } + + type StoreType = [number, string, string]; + const storeTypes: StoreType[] = [ + [1, 'google_play_store', 'Google Play Store'], + [2, 's3_bucket', 'Amazon S3 Bucket'], + [3, 'cloud', 'Cloud Provider'] + ]; + for (const [Id, Name, Description] of storeTypes) { + await prisma.storeTypes.upsert({ + where: { Id }, + update: {}, + create: { Id, Name, Description } + }); + } + + type StoreLanguage = [number, string, string, number]; + const storeLanguages: StoreLanguage[] = [ + [1, 'af', 'Afrikaans – af', 1], + [2, 'am', 'Amharic – am', 1], + [3, 'ar', 'Arabic – ar', 1], + [4, 'hy-AM', 'Armenian – hy-AM', 1], + [5, 'az-AZ', 'Azerbaijani – az-AZ', 1], + [6, 'eu-ES', 'Basque – eu-ES', 1], + [7, 'be', 'Belarusian – be', 1], + [8, 'bn-BD', 'Bengali – bn-BD', 1], + [9, 'bg', 'Bulgarian – bg', 1], + [10, 'my-MM', 'Burmese – my-MM', 1], + [11, 'ca', 'Catalan – ca', 1], + [12, 'zh-CN', 'Chinese (Simplified) – zh-CN', 1], + [13, 'zh-TW', 'Chinese (Traditional) – zh-TW', 1], + [14, 'hr', 'Croatian – hr', 1], + [15, 'cs-CZ', 'Czech – cs-CZ', 1], + [16, 'da-DK', 'Danish – da-DK', 1], + [17, 'nl-NL', 'Dutch – nl-NL', 1], + [18, 'en-AU', 'English – en-AU', 1], + [19, 'en-GB', 'English (United Kingdom) – en-GB', 1], + [20, 'en-US', 'English (United States) – en-US', 1], + [21, 'et', 'Estonian – et', 1], + [22, 'fil', 'Filipino – fil', 1], + [23, 'fi-FI', 'Finnish – fi-FI', 1], + [24, 'fr-FR', 'French – fr-FR', 1], + [25, 'fr-CA', 'French (Canada) – fr-CA', 1], + [26, 'gl-ES', 'Galician – gl-ES', 1], + [27, 'ka-GE', 'Georgian – ka-GE', 1], + [28, 'de-DE', 'German – de-DE', 1], + [29, 'el-GR', 'Greek – el-GR', 1], + [30, 'iw-IL', 'Hebrew – iw-IL', 1], + [31, 'hi-IN', 'Hindi – hi-IN', 1], + [32, 'hu-HU', 'Hungarian – hu-HU', 1], + [33, 'is-IS', 'Icelandic – is-IS', 1], + [34, 'id', 'Indonesian – id', 1], + [35, 'it-IT', 'Italian – it-IT', 1], + [36, 'ja-JP', 'Japanese – ja-JP', 1], + [37, 'kn-IN', 'Kannada – kn-IN', 1], + [38, 'km-KH', 'Khmer – km-KH', 1], + [39, 'ko-KR', 'Korean (South Korea) – ko-KR', 1], + [40, 'ky-KG', 'Kyrgyz – ky-KG', 1], + [41, 'lo-LA', 'Lao – lo-LA', 1], + [42, 'lv', 'Latvian – lv', 1], + [43, 'lt', 'Lithuanian – lt', 1], + [44, 'mk-MK', 'Macedonian – mk-MK', 1], + [45, 'ms', 'Malay – ms', 1], + [46, 'ml-IN', 'Malayalam – ml-IN', 1], + [47, 'mr-IN', 'Marathi – mr-IN', 1], + [48, 'mn-MN', 'Mongolian – mn-MN', 1], + [49, 'ne-NP', 'Nepali – ne-NP', 1], + [50, 'no-NO', 'Norwegian – no-NO', 1], + [51, 'fa', 'Persian – fa', 1], + [52, 'pl-PL', 'Polish – pl-PL', 1], + [53, 'pt-BR', 'Portuguese (Brazil) – pt-BR', 1], + [54, 'pt-PT', 'Portuguese (Portugal) – pt-PT', 1], + [55, 'ro', 'Romanian – ro', 1], + [56, 'rm', 'Romansh – rm', 1], + [57, 'ru-RU', 'Russian – ru-RU', 1], + [58, 'sr', 'Serbian – sr', 1], + [59, 'si-LK', 'Sinhala – si-LK', 1], + [60, 'sk', 'Slovak – sk', 1], + [61, 'sl', 'Slovenian – sl', 1], + [62, 'es-419', 'Spanish (Latin America) – es-419', 1], + [63, 'es-ES', 'Spanish (Spain) – es-ES', 1], + [64, 'es-US', 'Spanish (United States) – es-US', 1], + [65, 'sw', 'Swahili – sw', 1], + [66, 'sv-SE', 'Swedish – sv-SE', 1], + [67, 'ta-IN', 'Tamil – ta-IN', 1], + [68, 'te-IN', 'Telugu – te-IN', 1], + [69, 'th', 'Thai – th', 1], + [70, 'tr-TR', 'Turkish – tr-TR', 1], + [71, 'uk', 'Ukrainian – uk', 1], + [72, 'vi', 'Vietnamese – vi', 1], + [73, 'zu', 'Zulu – zu', 1] + ]; + for (const [Id, Name, Description, StoreTypeId] of storeLanguages) { + await prisma.storeLanguages.upsert({ + where: { Id }, + update: {}, + create: { Id, Name, Description, StoreTypeId } + }); + } + + // WorkflowDefnitions + const workflowDefinitionData = [ + { + Id: 1, + Name: 'sil_android_google_play', + Type: 1, + Enabled: true, + Description: 'SIL Default Workflow for Publishing to Google Play', + WorkflowScheme: 'SIL_Default_AppBuilders_Android_GooglePlay', + WorkflowBusinessFlow: 'SIL_Default_AppBuilders_Android_GooglePlay_Flow', + StoreTypeId: 1 + }, + { + Id: 2, + Name: 'sil_android_google_play_rebuild', + Type: 2, + Enabled: true, + Description: 'SIL Default Workflow for Rebuilding to Google Play', + WorkflowScheme: 'SIL_Default_AppBuilders_Android_GooglePlay_Rebuild', + WorkflowBusinessFlow: 'SIL_Default_AppBuilders_Android_GooglePlay_Flow', + StoreTypeId: 1 + }, + { + Id: 3, + Name: 'sil_android_google_play_republish', + Type: 3, + Enabled: true, + Description: 'SIL Default Workflow for Republish to Google Play', + WorkflowScheme: 'SIL_Default_AppBuilders_Android_GooglePlay_Republish', + WorkflowBusinessFlow: 'SIL_Default_AppBuilders_Android_GooglePlay_Flow', + StoreTypeId: 1 + }, + { + Id: 4, + Name: 'sil_android_s3', + Type: 1, + Enabled: true, + Description: 'SIL Default Workflow for Publish to Amazon S3 Bucket', + WorkflowScheme: 'SIL_Default_AppBuilders_Android_S3', + WorkflowBusinessFlow: 'SIL_Default_AppBuilders_Android_S3_Flow', + StoreTypeId: 2 + }, + { + Id: 5, + Name: 'sil_android_s3_rebuild', + Type: 2, + Enabled: true, + Description: 'SIL Default Workflow for Rebuilding to Amazon S3 Bucket', + WorkflowScheme: 'SIL_Default_AppBuilders_Android_S3_Rebuild', + WorkflowBusinessFlow: 'SIL_Default_AppBuilders_Android_S3_Flow', + StoreTypeId: 2 + }, + { + Id: 6, + Name: 'la_android_google_play', + Type: 1, + Enabled: true, + Description: 'Low Admin Workflow for Publishing to Google Play', + WorkflowScheme: 'SIL_LowAdmin_AppBuilders_Android_GooglePlay', + WorkflowBusinessFlow: 'SIL_Default_AppBuilders_Android_GooglePlay_Flow', + StoreTypeId: 1 + }, + { + Id: 7, + Name: 'oa_android_google_play', + Type: 1, + Enabled: true, + Description: 'Owner Admin Workflow for Publishing to Google Play', + WorkflowScheme: 'SIL_OwnerAdmin_AppBuilders_Android_GooglePlay', + WorkflowBusinessFlow: 'SIL_Default_AppBuilders_Android_GooglePlay_Flow', + StoreTypeId: 1 + }, + { + Id: 8, + Name: 'na_android_s3', + Type: 1, + Enabled: true, + Description: 'No Admin Workflow for Publishing to S3', + WorkflowScheme: 'SIL_NoAdmin_AppBuilders_Android_S3', + WorkflowBusinessFlow: 'SIL_Default_AppBuilders_Android_S3_Flow', + StoreTypeId: 2 + }, + { + Id: 9, + Name: 'pwa_cloud', + Type: 1, + Enabled: true, + Description: 'SIL Default Workflow for Publishing PWA to Cloud', + WorkflowScheme: 'SIL_Default_AppBuilders_Pwa_Cloud', + WorkflowBusinessFlow: 'SIL_AppBuilders_Web_Flow', + StoreTypeId: 3 + }, + { + Id: 10, + Name: 'pwa_cloud_rebuild', + Type: 2, + Enabled: true, + Description: 'SIL Default Workflow for Rebuilding PWA to Cloud', + WorkflowScheme: 'SIL_Default_AppBuilders_Pwa_Cloud_Rebuild', + WorkflowBusinessFlow: 'SIL_AppBuilders_Web_Flow', + StoreTypeId: 3 + }, + { + Id: 11, + Name: 'html_cloud', + Type: 1, + Enabled: true, + Description: 'SIL Default Workflow for Publishing HTML to Cloud', + WorkflowScheme: 'SIL_Default_AppBuilders_Html_Cloud', + WorkflowBusinessFlow: 'SIL_AppBuilders_Web_Flow', + StoreTypeId: 3 + }, + { + Id: 12, + Name: 'html_cloud_rebuild', + Type: 2, + Enabled: true, + Description: 'SIL Default Workflow for Rebuilding HTML to Cloud', + WorkflowScheme: 'SIL_Default_AppBuilders_Html_Cloud_Rebuild', + WorkflowBusinessFlow: 'SIL_AppBuilders_Web_Flow', + StoreTypeId: 3 + }, + { + Id: 13, + Name: 'asset_package', + Type: 1, + Enabled: true, + Description: 'SIL Default Workflow for Publishing Asset Packages', + WorkflowScheme: 'SIL_NoAdmin_AppBuilders_Android_S3', + WorkflowBusinessFlow: 'SIL_AppBuilders_AssetPackage_Flow', + StoreTypeId: 2, + Properties: '{ "build:targets" : "asset-package" }' + }, + { + Id: 14, + Name: 'asset_package_rebuild', + Type: 2, + Enabled: true, + Description: 'SIL Default Workflow for Rebuilding Asset Packages', + WorkflowScheme: 'SIL_Default_AppBuilders_Android_S3_Rebuild', + WorkflowBusinessFlow: 'SIL_AppBuilders_AssetPackage_Flow', + StoreTypeId: 2, + Properties: '{ "build:targets" : "asset-package" }' + } + ]; + + for (const data of workflowDefinitionData) { + await prisma.workflowDefinitions.upsert({ + where: { Id: data.Id }, + update: data, + create: data + }); + } + + const productDefinitionData = [ + { + Id: 1, + Name: 'Android App to Google Play', + TypeId: 1, + Description: + 'Build an Android App from a Scripture App Builder project and publish to a Google Play Store. The Organization Admin has to approve of the project and review the store preview. The Organization Admin has access to Google Play Console.', + WorkflowId: 1, + RebuildWorkflowId: 2, + RepublishWorkflowId: 3 + }, + { + Id: 2, + Name: 'Android App to Amazon S3 Bucket', + TypeId: 1, + Description: + 'Build an Android App from a Scripture App Builder project and publish to an Amazon S3 Bucket', + WorkflowId: 4, + RebuildWorkflowId: 5 + }, + { + Id: 3, + Name: 'Android App to Google Play (Low Admin)', + TypeId: 1, + Description: + 'Build an Android App from a Scripture App Builder project and publish to a Google Play Store, but with less approval and oversight required. The Organization Admin has access to Google Play Console.', + WorkflowId: 6, + RebuildWorkflowId: 2, + RepublishWorkflowId: 3 + }, + { + Id: 4, + Name: 'Android App to Amazon S3 Bucket (No Admin)', + TypeId: 1, + Description: + 'Build an Android App from a Scripture App Builder project and publish to an Amazon S3 Bucket, but with no admin required.', + WorkflowId: 8, + RebuildWorkflowId: 5 + }, + { + Id: 5, + Name: 'Android App to Google Play (Owner Admin)', + TypeId: 1, + Description: + 'Build an Android App from a Scripture App Builder project and publish to a Google Play Store, but with no approval and oversight required. The owner of the project has access to Google Play Console.', + WorkflowId: 7, + RebuildWorkflowId: 2, + RepublishWorkflowId: 3 + } + ]; + + for (const data of productDefinitionData) { + await prisma.productDefinitions.upsert({ + where: { Id: data.Id }, + update: data, + create: data + }); + } + + if (options.organizations) { + const usersData = [ + { + Id: 1, + Name: 'Chris Hubbard', + Email: 'chris_hubbard@sil.org', + ExternalId: 'google-oauth2|116747902156680384840', + FamilyName: 'Hubbard', + GivenName: 'Chris', + IsLocked: false + }, + { + Id: 2, + Name: 'David Moore', + Email: 'david_moore1@sil.org', + ExternalId: 'google-oauth2|114981819181509824425', + FamilyName: 'Moore', + GivenName: 'David', + IsLocked: false + }, + { + Id: 3, + Name: 'Bill Dyck', + Email: 'bill_dyck@sil.org', + ExternalId: 'google-oauth2|102643649500459434996', + FamilyName: 'Dyck', + GivenName: 'Bill', + IsLocked: false + }, + { + Id: 4, + Name: 'Loren Hawthorne', + Email: 'loren_hawthorne@sil.org', + ExternalId: 'google-oauth2|116603781884964961816', + FamilyName: 'Hawthorne', + GivenName: 'Loren', + IsLocked: false + }, + { + Id: 5, + Name: 'Chris Hubbard (Kalaam)', + Email: 'chris.kalaam@gmail.com', + ExternalId: 'auth0|5b578f6197af652b19f9bb41', + FamilyName: 'Hubbard', + GivenName: 'Chris', + IsLocked: false + } + ]; + + for (const userData of usersData) { + await prisma.users.upsert({ + where: { Id: userData.Id }, + update: userData, + create: userData + }); + } + + const organizationsData = [ + { + Id: 1, + Name: 'SIL International', + WebsiteUrl: 'https://sil.org', + BuildEngineUrl: 'https://replace.scriptoria.io:8443', + BuildEngineApiAccessToken: 'replace', + LogoUrl: 'https://sil-prd-aps-resources.s3.amazonaws.com/sil/sil_logo.png', + OwnerId: 1 + }, + { + Id: 2, + Name: 'Kalaam Media', + WebsiteUrl: 'https://kalaam.org', + BuildEngineUrl: 'https://replace.scriptoria.io:8443', + BuildEngineApiAccessToken: 'replace', + LogoUrl: 'https://s3.amazonaws.com/sil-prd-aps-resources/ips/wildfire_flame_logo.png', + OwnerId: 1 + }, + { + Id: 3, + Name: 'Faith Comes By Hearing', + WebsiteUrl: 'https://kalaam.org', + BuildEngineUrl: 'https://replace.scriptoria.io:8443', + BuildEngineApiAccessToken: 'replace', + LogoUrl: + 'https://play-lh.googleusercontent.com/yXm_WKG7_DJxL3IPYFul6iYfRNzWGdSbOJad7acWt28Xc6jSdlJCXPgrJOc-mdkf5_OE', + OwnerId: 1 + }, + { + Id: 4, + Name: 'Scripture Earth', + WebsiteUrl: 'https://scriptureearth.org', + BuildEngineUrl: 'https://replace.scriptoria.io:8443', + BuildEngineApiAccessToken: 'replace', + LogoUrl: 'https://s3.amazonaws.com/sil-prd-aps-resources/scriptureearth/se_logo.png', + OwnerId: 1 + } + ]; + + for (const organizationData of organizationsData) { + await prisma.organizations.upsert({ + where: { Id: organizationData.Id }, + update: organizationData, + create: organizationData + }); + } + + const storesData = [ + { + Id: 1, + Name: 'wycliffeusa', + Description: 'Wycliffe USA - Google Play', + StoreTypeId: 1 + }, + { + Id: 2, + Name: 'internetpublishingservice', + Description: 'Internet Publishing Service (Kalaam) - Google Play', + StoreTypeId: 1 + }, + { Id: 3, Name: 'indhack', Description: 'Indigitous Hack - Google Play', StoreTypeId: 1 } + ]; + + for (const storeData of storesData) { + await prisma.stores.upsert({ + where: { Id: storeData.Id }, + update: storeData, + create: storeData + }); + } + + const organizationProductDefinitionsData = [ + { Id: 1, OrganizationId: 1, ProductDefinitionId: 1 }, + { Id: 2, OrganizationId: 2, ProductDefinitionId: 1 }, + { Id: 3, OrganizationId: 3, ProductDefinitionId: 1 } + ]; + + for (const data of organizationProductDefinitionsData) { + await prisma.organizationProductDefinitions.upsert({ + where: { + Id: data.Id + }, + update: data, + create: data + }); + } + + const organizationStoresData = [ + { Id: 1, OrganizationId: 1, StoreId: 1 }, + { Id: 2, OrganizationId: 2, StoreId: 2 }, + { Id: 3, OrganizationId: 3, StoreId: 3 }, + { Id: 4, OrganizationId: 4, StoreId: 3 } + ]; + + for (const data of organizationStoresData) { + await prisma.organizationStores.upsert({ + where: { Id: data.Id }, + update: data, + create: data + }); + } + + const groupsData = [ + { Id: 1, Name: 'Language Software Development', Abbreviation: 'LSDEV', OwnerId: 1 }, + { Id: 2, Name: 'Chad Branch', Abbreviation: 'CHB', OwnerId: 1 }, + { Id: 3, Name: 'Mexico Branch', Abbreviation: 'MXB', OwnerId: 1 }, + { Id: 4, Name: 'AuSIL', Abbreviation: 'AAB', OwnerId: 1 }, + { Id: 5, Name: 'Americas Group', Abbreviation: 'AMG', OwnerId: 1 }, + { Id: 6, Name: 'Asia Area', Abbreviation: 'ASA', OwnerId: 1 }, + { Id: 7, Name: 'Camaroon Branch', Abbreviation: 'CMB', OwnerId: 1 }, + { Id: 8, Name: 'Roma Region', Abbreviation: 'RMA', OwnerId: 1 }, + { Id: 9, Name: 'The Seed Company', Abbreviation: 'RSM', OwnerId: 1 }, + { Id: 10, Name: 'SIL International', Abbreviation: 'SIL', OwnerId: 1 }, + { Id: 11, Name: 'Mainland Southeast Asia Group', Abbreviation: 'THG', OwnerId: 1 }, + { Id: 12, Name: 'Wycliffe Taiwan', Abbreviation: 'TWN', OwnerId: 1 }, + { Id: 13, Name: 'West Africa', Abbreviation: 'WAF', OwnerId: 1 }, + { Id: 14, Name: 'Kalaam', Abbreviation: 'KAL', OwnerId: 2 }, + { Id: 15, Name: 'FCBH', Abbreviation: 'FCBH', OwnerId: 3 }, + { Id: 16, Name: 'Scripture Earth', Abbreviation: 'SE', OwnerId: 4 } + ]; + + for (const data of groupsData) { + await prisma.groups.upsert({ + where: { Id: data.Id }, + update: data, + create: data + }); + } + + const organizationMembershipsData = [ + { Id: 1, UserId: 1, OrganizationId: 1 }, // chris_hubbard@sil.org - SIL + { Id: 2, UserId: 1, OrganizationId: 2 }, // chris_hubbard@sil.org - Kalaam + { Id: 3, UserId: 1, OrganizationId: 3 }, // chris_hubbard@sil.org - FCBH + { Id: 4, UserId: 2, OrganizationId: 1 }, // david_moore1@sil.org - SIL + { Id: 5, UserId: 3, OrganizationId: 1 }, // bill_dyck@sil.org - SIL + { Id: 6, UserId: 3, OrganizationId: 4 }, // bill_dyck@sil.org - SE + { Id: 7, UserId: 4, OrganizationId: 1 }, // loren_hawthorne@sil.org - SIL + { Id: 8, UserId: 5, OrganizationId: 2 } // chris.kalaam@gmail.com - Kalaam + ]; + + for (const data of organizationMembershipsData) { + await prisma.organizationMemberships.upsert({ + where: { Id: data.Id }, + update: data, + create: data + }); + } + + const groupMembershipsData = [ + { Id: 1, UserId: 1, GroupId: 1 }, + { Id: 2, UserId: 2, GroupId: 1 }, + { Id: 3, UserId: 3, GroupId: 5 }, + { Id: 4, UserId: 3, GroupId: 6 }, + { Id: 5, UserId: 3, GroupId: 11 }, + { Id: 6, UserId: 3, GroupId: 12 }, + { Id: 7, UserId: 3, GroupId: 16 }, + { Id: 8, UserId: 4, GroupId: 3 }, + { Id: 9, UserId: 5, GroupId: 14 } + ]; + + for (const data of groupMembershipsData) { + await prisma.groupMemberships.upsert({ + where: { Id: data.Id }, + update: data, + create: data + }); + } + + const userRolesData = [ + { Id: 1, UserId: 1, RoleId: 1, OrganizationId: 1 }, // chris_hubbard@sil.org - SuperAdmin - SIL + { Id: 2, UserId: 1, RoleId: 2, OrganizationId: 1 }, // chris_hubbard@sil.org - OrgAdmin - SIL + { Id: 3, UserId: 1, RoleId: 1, OrganizationId: 2 }, // chris_hubbard@sil.org - SuperAdmin - Kalaam + { Id: 4, UserId: 1, RoleId: 1, OrganizationId: 3 }, // chris_hubbard@sil.org - SuperAdmin - FCBH + { Id: 5, UserId: 1, RoleId: 1, OrganizationId: 4 }, // chris_hubbard@sil.org - SuperAdmin - SE + { Id: 6, UserId: 2, RoleId: 1, OrganizationId: 1 }, // david_moore1@sil.org - SuperAdmin - SIL + { Id: 7, UserId: 2, RoleId: 2, OrganizationId: 1 }, // david_moore1@sil.org - OrgAdmin - SIL + { Id: 8, UserId: 3, RoleId: 2, OrganizationId: 1 }, // bill_dyck@sil.org - OrgAdmin - SIL + { Id: 9, UserId: 3, RoleId: 2, OrganizationId: 4 }, // bill_dyck@sil.org - OrgAdmin - SE + { Id: 10, UserId: 4, RoleId: 3, OrganizationId: 1 }, // loren_hawthorne@sil.org - AppBuilder - SIL + { Id: 11, UserId: 5, RoleId: 3, OrganizationId: 3 } // chris.kalaam@gmail.com - AppBuilder - Kalaam + ]; + + for (const data of userRolesData) { + await prisma.userRoles.upsert({ + where: { Id: data.Id }, + update: data, + create: data + }); + } + } +} +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/source/SIL.AppBuilder.Portal/common/prismaTypes.ts b/source/SIL.AppBuilder.Portal/common/prismaTypes.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/SIL.AppBuilder.Portal/common/public/prisma.ts b/source/SIL.AppBuilder.Portal/common/public/prisma.ts new file mode 100644 index 0000000000..106ce3ffaa --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/public/prisma.ts @@ -0,0 +1,22 @@ +export enum RoleId { + SuperAdmin = 1, + OrgAdmin, + AppBuilder, + Author +} + +export enum ProductTransitionType { + Activity = 1, + StartWorkflow, + EndWorkflow, + CancelWorkflow, + ProjectAccess +} + +export enum WorkflowType { + Startup = 1, + Rebuild, + Republish +} + +export const WorkflowTypeString = ['', 'Startup', 'Rebuild', 'Republish']; diff --git a/source/SIL.AppBuilder.Portal/common/public/utils.ts b/source/SIL.AppBuilder.Portal/common/public/utils.ts new file mode 100644 index 0000000000..66bfe2a61d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/public/utils.ts @@ -0,0 +1,35 @@ +export type ValueFilter = + | { is: T } // T is the provided + | { any: Set } // T is any of the provided + | { not: T } // T is not the provided + | { none: Set }; // T is none of the provided + +export type SetFilter = + | { has: T } // Set contains the provided + | { any: Set } // Set contains any of the provided + | { all: Set } // Set contains all of the provided + | { none: Set }; // Set contains none of the provided + +export function filterValue(value: T, filter: ValueFilter) { + if ('is' in filter) { + return value === filter.is; + } else if ('any' in filter) { + return filter.any.has(value); + } else if ('not' in filter) { + return value !== filter.not; + } else { + return !filter.none.has(value); + } +} + +export function filterSet(values: Set, filter: SetFilter) { + if ('has' in filter) { + return values.has(filter.has); + } else if ('any' in filter) { + return !values.isDisjointFrom(filter.any); + } else if ('all' in filter) { + return values.isSupersetOf(filter.all); + } else { + return values.isDisjointFrom(filter.none); + } +} diff --git a/source/SIL.AppBuilder.Portal/common/public/workflow.ts b/source/SIL.AppBuilder.Portal/common/public/workflow.ts new file mode 100644 index 0000000000..3b18d4bde9 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/public/workflow.ts @@ -0,0 +1,245 @@ +import { and, type TransitionConfig } from 'xstate'; +import type { RoleId, WorkflowType } from './prisma.js'; +import type { SetFilter, ValueFilter } from './utils.js'; +import { filterSet, filterValue } from './utils.js'; + +export enum ActionType { + /** Automated Action */ + Auto = 0, + /** User-initiated Action */ + User +} + +/** + * Optional features of the workflow. Different states/transitions can be included based on provided options. + */ +export enum WorkflowOptions { + /** Require an OrgAdmin to access the GooglePlay Developer Console */ + AdminStoreAccess = 1, + /** Require approval from an OrgAdmin before product can be created */ + ApprovalProcess, + /** Allow Owner to delegate actions to Authors */ + AllowTransferToAuthors +} + +export enum ProductType { + Android_GooglePlay = 0, + Android_S3, + AssetPackage, + Web +} + +export enum WorkflowState { + Start = 'Start', + Approval = 'Approval', + Approval_Pending = 'Approval Pending', + Terminated = 'Terminated', + Product_Creation = 'Product Creation', + App_Builder_Configuration = 'App Builder Configuration', + Author_Configuration = 'Author Configuration', + Synchronize_Data = 'Synchronize Data', + Author_Download = 'Author Download', + Author_Upload = 'Author Upload', + Product_Build = 'Product Build', + App_Store_Preview = 'App Store Preview', + Create_App_Store_Entry = 'Create App Store Entry', + Verify_and_Publish = 'Verify and Publish', + Product_Publish = 'Product Publish', + Make_It_Live = 'Make It Live', + Published = 'Published' +} + +export const TerminalStates = [WorkflowState.Terminated, WorkflowState.Published]; + +export enum WorkflowAction { + Default = 'Default', + Continue = 'Continue', + Approve = 'Approve', + Hold = 'Hold', + Reject = 'Reject', + Jump = 'Jump', + Product_Created = 'Product Created', + New_App = 'New App', + Existing_App = 'Existing App', + Transfer_to_Authors = 'Transfer to Authors', + Take_Back = 'Take Back', + Build_Successful = 'Build Successful', + Build_Failed = 'Build Failed', + Email_Reviewers = 'Email Reviewers', + Publish_Completed = 'Publish Completed', + Publish_Failed = 'Publish Failed' +} + +export type WorkflowInstanceContext = { + instructions: + | 'asset_package_verify_and_publish' + | 'app_configuration' + | 'create_app_entry' + | 'authors_download' + | 'googleplay_configuration' + | 'googleplay_verify_and_publish' + | 'make_it_live' + | 'approval_pending' + | 'readiness_check' + | 'synchronize_data' + | 'authors_upload' + | 'verify_and_publish' + | 'waiting' + | 'web_verify' + | null; + includeFields: ( + | 'ownerName' + | 'ownerEmail' + | 'storeDescription' + | 'listingLanguageCode' + | 'projectURL' + | 'productDescription' + | 'appType' + | 'projectLanguageCode' + )[]; + includeReviewers: boolean; + includeArtifacts: 'apk' | 'aab' | boolean; + start?: WorkflowState; + environment: Environment; +}; + +export type Environment = { [key: ENVKeys | string]: string }; + +export enum ENVKeys { + // Set by Workflow + PUBLISH_GOOGLE_PLAY_UPLOADED_BUILD_ID = 'PUBLISH_GOOGLE_PLAY_UPLOADED_BUILD_ID', + PUBLISH_GOOGLE_PLAY_UPLOADED_VERSION_CODE = 'PUBLISH_GOOGLE_PLAY_UPLOADED_VERSION_CODE', + GOOGLE_PLAY_EXISTING = 'google_play_existing', + GOOGLE_PLAY_DRAFT = 'PUBLISH_GOOGLE_PLAY_DRAFT', + // Before Build + UI_URL = 'UI_URL', + PRODUCT_ID = 'PRODUCT_ID', + PROJECT_ID = 'PROJECT_ID', + PROJECT_NAME = 'PROJECT_NAME', + PROJECT_DESCRIPTION = 'PROJECT_DESCRIPTION', + PROJECT_URL = 'PROJECT_URL', + PROJECT_LANGUAGE = 'PROJECT_LANGUAGE', + PROJECT_ORGANIZATION = 'PROJECT_ORGANIZATION', + PROJECT_OWNER_NAME = 'PROJECT_OWNER_NAME', + PROJECT_OWNER_EMAIL = 'PROJECT_OWNER_EMAIL' +} + +export type WorkflowContext = WorkflowInstanceContext & WorkflowInput; + +export type WorkflowConfig = { + options: Set; + productType: ProductType; + workflowType: WorkflowType; +}; + +export type WorkflowInput = WorkflowConfig & { + productId: string; + hasAuthors: boolean; + hasReviewers: boolean; +}; + +/** Used for filtering based on specified WorkflowOptions and/or ProductType */ +export type MetaFilter = { + options?: SetFilter; + productType?: ValueFilter; + workflowType?: ValueFilter; +}; + +export type WorkflowStateMeta = { includeWhen?: MetaFilter }; + +export type WorkflowTransitionMeta = { + type: ActionType; + user?: RoleId; + includeWhen?: MetaFilter; +}; + +/** + * Include state/transition if: + * - no conditions are specified + * - all specified conditions are met + */ +export function includeStateOrTransition(config: WorkflowConfig, filter?: MetaFilter) { + let include = !!filter; + if (!filter) { + return true; // no conditions are specified + } + if (include && filter.options) { + include &&= filterSet(config.options, filter.options); + } + if (include && filter.productType) { + include &&= filterValue(config.productType, filter.productType); + } + if (include && filter.workflowType) { + include &&= filterValue(config.workflowType, filter.workflowType); + } + return include; +} + +export type WorkflowEvent = { + type: WorkflowAction; + comment?: string; + target?: WorkflowState; + userId: number | null; +}; + +export type JumpParams = { + target: WorkflowState | string; + filter?: MetaFilter; +}; + +export function canJump(args: { context: WorkflowContext }, params: JumpParams): boolean { + return ( + args.context.start === params.target && includeStateOrTransition(args.context, params.filter) + ); +} +export function hasAuthors(args: { context: WorkflowContext }): boolean { + return args.context.hasAuthors; +} +export function hasReviewers(args: { context: WorkflowContext }): boolean { + return args.context.hasReviewers; +} +/** + * @param params expected params of `canJump` guard from StartupWorkflow + * @param optionalGuards other guards that can optionally be added. + * @returns A properly configured object for the `always` array of the `Start` state for jumping to an arbitrary state. + */ +export function jump( + params: JumpParams, + optionalGuards?: (typeof hasAuthors | typeof hasReviewers)[] +): + | TransitionConfig< + WorkflowContext, + WorkflowEvent, + WorkflowEvent, + never, + never, + never, + never, + WorkflowEvent, + WorkflowStateMeta | WorkflowTransitionMeta + > + | string { + const j = (args: { context: WorkflowContext }) => canJump(args, params); + return { + guard: optionalGuards ? and(optionalGuards.concat([j])) : j, + target: params.target + }; +} + +export type StateNode = { + id: number; + label: string; // TODO: i18n (after MVP) + connections: { id: number; target: string; label: string }[]; + inCount: number; + start?: boolean; + final?: boolean; + action?: boolean; +}; + +export type Snapshot = { + instanceId: number; + definitionId: number; + state: string; + context: WorkflowInstanceContext; + config: WorkflowConfig; +}; diff --git a/source/SIL.AppBuilder.Portal/common/tsconfig.json b/source/SIL.AppBuilder.Portal/common/tsconfig.json new file mode 100644 index 0000000000..153ca65612 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "module": "nodenext", + "skipLibCheck": true, + "strict": true + }, + "exclude": ["prisma"] +} diff --git a/source/SIL.AppBuilder.Portal/common/workflow/index.ts b/source/SIL.AppBuilder.Portal/common/workflow/index.ts new file mode 100644 index 0000000000..9485c39b95 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/workflow/index.ts @@ -0,0 +1,560 @@ +import { Prisma } from '@prisma/client'; +import type { + Actor, + InspectedSnapshotEvent, + TransitionDefinition, + TransitionDefinitionMap, + StateNode as XStateNode +} from 'xstate'; +import { createActor } from 'xstate'; +import DatabaseWrites from '../databaseProxy/index.js'; +import { allUsersByRole } from '../databaseProxy/UserRoles.js'; +import { BullMQ, Queues } from '../index.js'; +import prisma from '../prisma.js'; +import { ProductTransitionType, RoleId, WorkflowType } from '../public/prisma.js'; +import type { + Snapshot, + StateNode, + WorkflowConfig, + WorkflowContext, + WorkflowEvent, + WorkflowInstanceContext +} from '../public/workflow.js'; +import { + ActionType, + includeStateOrTransition, + TerminalStates, + WorkflowState +} from '../public/workflow.js'; +import { WorkflowStateMachine } from './state-machine.js'; + +/** + * Wraps a workflow instance and provides methods to interact. + */ +export class Workflow { + private flow: Actor | null; + private productId: string; + private currentState: XStateNode | null; + private config: WorkflowConfig; + + private constructor(productId: string, config: WorkflowConfig) { + this.flow = null; // to make svelte-check happy + this.currentState = null; // ^^^ + this.productId = productId; + this.config = config; + } + + /* PUBLIC METHODS */ + /** Create a new workflow instance and populate the database tables. */ + public static async create(productId: string, config: WorkflowConfig): Promise { + const flow = new Workflow(productId, config); + + const check = await flow.checkAuthorsAndReviewers(); + + flow.flow = createActor(WorkflowStateMachine, { + inspect: (e) => { + if (e.type === '@xstate.snapshot') flow.inspect(e); + }, + input: { + productId, + hasAuthors: check._count.Authors > 0, + hasReviewers: check._count.Authors > 0, + ...config + } + }); + + flow.flow.start(); + await flow.createSnapshot(flow.flow.getSnapshot().context); + await DatabaseWrites.productTransitions.create({ + data: { + ProductId: productId, + DateTransition: new Date(), + TransitionType: ProductTransitionType.StartWorkflow, + WorkflowType: config.workflowType + } + }); + await Queues.UserTasks.add(`Create UserTasks for Product #${productId}`, { + type: BullMQ.JobType.UserTasks_Modify, + scope: 'Product', + productId: productId, + operation: { + type: BullMQ.UserTasks.OpType.Create + } + }); + + return flow; + } + /** Restore from a snapshot in the database. */ + public static async restore(productId: string): Promise { + const snap = await Workflow.getSnapshot(productId); + if (!snap) { + return null; + } + const flow = new Workflow(productId, snap.config); + const check = await flow.checkAuthorsAndReviewers(); + flow.flow = createActor(WorkflowStateMachine, { + snapshot: snap + ? WorkflowStateMachine.resolveState({ + value: snap.state, + context: { + ...snap.context, + ...snap.config, + productId: productId, + hasAuthors: check._count.Authors > 0, + hasReviewers: check._count.Authors > 0 + } + }) + : undefined, + inspect: (e) => { + if (e.type === '@xstate.snapshot') flow.inspect(e); + }, + input: { + ...snap.config, + productId: productId, + hasAuthors: check._count.Authors > 0, + hasReviewers: check._count.Authors > 0 + } + }); + + flow.flow.start(); + + return flow; + } + + public static async delete(productId: string) { + const product = await prisma.products.findUnique({ + where: { Id: productId }, + select: { + ProjectId: true, + WorkflowInstance: { select: { WorkflowDefinition: { select: { Type: true } } } } + } + }); + if (product?.WorkflowInstance) { + await DatabaseWrites.workflowInstances.delete(productId, product.ProjectId); + await DatabaseWrites.productTransitions.create({ + data: { + ProductId: productId, + // This is how S1 does it. May want to change later + AllowedUserNames: '', + DateTransition: new Date(), + TransitionType: ProductTransitionType.EndWorkflow, + WorkflowType: product.WorkflowInstance.WorkflowDefinition.Type + } + }); + } + } + + /** Send a transition event to the workflow. */ + public send(event: WorkflowEvent): void { + this.flow?.send(event); + } + + /** + * Stops the current running workflow. + * + * Note: This does not mean that the workflow is terminated. + */ + public stop(): void { + this.flow?.stop(); + } + + /** Retrieves the workflow's snapshot from the database */ + public static async getSnapshot(productId: string): Promise { + const instance = await prisma.workflowInstances.findUnique({ + where: { + ProductId: productId + }, + select: { + Id: true, + State: true, + Context: true, + WorkflowDefinition: { + select: { + Id: true, + ProductType: true, + WorkflowOptions: true, + Type: true + } + } + } + }); + if (!instance) { + return null; + } + return { + instanceId: instance.Id, + definitionId: instance.WorkflowDefinition.Id, + state: instance.State, + context: JSON.parse(instance.Context) as WorkflowInstanceContext, + config: { + workflowType: instance.WorkflowDefinition.Type, + productType: instance.WorkflowDefinition.ProductType, + options: new Set(instance.WorkflowDefinition.WorkflowOptions) + } + }; + } + + /** Returns the name of the current state */ + public state() { + return this.flow?.getSnapshot().value; + } + + /** Returns a list of valid transitions from the provided state. */ + public static availableTransitionsFromName(stateName: string, config: WorkflowConfig) { + return Workflow.availableTransitionsFromNode( + WorkflowStateMachine.getStateNodeById(Workflow.stateIdFromName(stateName)), + config + ); + } + + public static availableTransitionsFromNode( + s: XStateNode, + config: WorkflowConfig + ) { + return Workflow.filterTransitions(s.on, config); + } + + /** Transform state machine definition into something more easily usable by the visualization algorithm */ + public serializeForVisualization(): StateNode[] { + const states = Object.entries(WorkflowStateMachine.states).filter(([k, v]) => + includeStateOrTransition(this.config, v.meta?.includeWhen) + ); + const lookup = states.map((s) => s[0]); + const actions: StateNode[] = []; + return states + .map(([k, v]) => { + return { + id: lookup.indexOf(k), + label: k, + connections: Workflow.filterTransitions(v.on, this.config).map((o) => { + let target = Workflow.targetStringFromEvent(o[0]); + if (!target) { + target = o[0].eventType; + lookup.push(target); + actions.push({ + id: lookup.lastIndexOf(target), + label: target, + connections: [ + { + id: lookup.indexOf(k), + target: k, + label: '' + } + ], + inCount: 1, + action: true + }); + } + return { + // treat no target on transition as self target + id: lookup.lastIndexOf(target), + target: target, + label: o[0].eventType + }; + }), + inCount: states + .map(([k, v]) => { + return Workflow.filterTransitions(v.on, this.config).map((e) => { + // treat no target on transition as self target + return { from: k, to: Workflow.targetStringFromEvent(e[0]) || k }; + }); + }) + .reduce((p, c) => { + return p.concat(c); + }, []) + .filter((v) => k === v.to).length, + start: k === WorkflowState.Start, + final: v.type === 'final' + } as StateNode; + }) + .concat(actions); + } + + /* PRIVATE METHODS */ + private async inspect(event: InspectedSnapshotEvent): Promise { + const old = this.currentState; + const xSnap = this.flow!.getSnapshot(); + this.currentState = WorkflowStateMachine.getStateNodeById( + `#${WorkflowStateMachine.id}.${xSnap.value}` + ); + + if (old && Workflow.stateName(old) !== xSnap.value) { + await this.updateProductTransitions( + event.event.userId, + Workflow.stateName(old), + Workflow.stateName(this.currentState), + event.event.type, + event.event.comment || undefined + ); + await DatabaseWrites.productTransitions.deleteMany({ + where: { + ProductId: this.productId, + DateTransition: null, + WorkflowUserId: null + } + }); + // Yes, the ModifyUserTasks will also delete tasks. I just have this here so the tasks are cleared immediately, and so that the tasks are also cleared when the instance is deleted. + await DatabaseWrites.userTasks.deleteMany({ + where: { + ProductId: this.productId + } + }); + if (TerminalStates.includes(xSnap.value)) { + // This code will probably never be reachable? + // It looks like the inspect hook is not invoked when a final state is reached... + await Workflow.delete(this.productId); + } else { + await this.createSnapshot(xSnap.context); + // This will also create the dummy entries in the ProductTransitions table + await Queues.UserTasks.add(`Update UserTasks for Product #${this.productId}`, { + type: BullMQ.JobType.UserTasks_Modify, + scope: 'Product', + productId: this.productId, + comment: event.event.comment || undefined, + operation: { + type: BullMQ.UserTasks.OpType.Update + } + }); + } + } + } + + private async createSnapshot(context: WorkflowContext) { + const filtered = { + ...context, + productId: undefined, + hasAuthors: undefined, + hasReviewers: undefined, + productType: undefined, + options: undefined + } as WorkflowInstanceContext; + const prodDefinition = (await prisma.products.findUnique({ + where: { + Id: this.productId + }, + select: { + ProductDefinition: { + select: { + WorkflowId: true, + RebuildWorkflowId: true, + RepublishWorkflowId: true + } + } + } + }))!.ProductDefinition; + return DatabaseWrites.workflowInstances.upsert(this.productId, { + create: { + State: Workflow.stateName(this.currentState!), + Context: JSON.stringify(context), + WorkflowDefinitionId: + context.workflowType === WorkflowType.Rebuild + ? prodDefinition.RebuildWorkflowId! + : context.workflowType === WorkflowType.Republish + ? prodDefinition.RepublishWorkflowId! + : prodDefinition.WorkflowId! + }, + update: { + State: Workflow.stateName(this.currentState!), + Context: JSON.stringify(filtered) + } + }); + } + + /** Filter a states transitions based on provided context */ + private static filterTransitions( + on: TransitionDefinitionMap, + filter: WorkflowConfig + ) { + return Object.values(on) + .map((v) => v.filter((t) => includeStateOrTransition(filter, t.meta?.includeWhen))) + .filter((v) => v.length > 0 && includeStateOrTransition(filter, v[0].meta?.includeWhen)); + } + + /** Create ProductTransitions record object */ + private static transitionFromState( + state: XStateNode, + productId: string, + config: WorkflowConfig, + users: Record> + ): Prisma.ProductTransitionsCreateManyInput { + const t = Workflow.filterTransitions(state.on, config)[0][0]; + + return { + ProductId: productId, + AllowedUserNames: + t.meta.type === ActionType.User + ? Array.from( + new Set( + Object.entries(users) + .filter(([user, roles]) => roles.has(t.meta.user)) + .map(([user, roles]) => user) + ) + ).join() + : null, + TransitionType: ProductTransitionType.Activity, + InitialState: Workflow.stateName(state), + DestinationState: Workflow.targetStringFromEvent(t), + Command: t.meta.type !== ActionType.Auto ? t.eventType : null, + WorkflowType: config.workflowType + }; + } + + public static async transitionEntriesFromState( + stateName: string, + productId: string, + config: WorkflowConfig + ): Promise { + const projectId = (await prisma.products.findUnique({ + where: { + Id: productId + }, + select: { + ProjectId: true + } + }))!.ProjectId; + const allUsers = await allUsersByRole(projectId); + const users = Object.fromEntries( + ( + await prisma.users.findMany({ + where: { + Id: { in: Object.keys(allUsers).map((str) => parseInt(str)) } + }, + select: { + Id: true, + Name: true + } + }) + ).map((u) => [u.Name, allUsers[u.Id]]) + ); + const ret: Prisma.ProductTransitionsCreateManyInput[] = [ + Workflow.transitionFromState( + stateName === WorkflowState.Start + ? Workflow.availableTransitionsFromName(WorkflowState.Start, config)[0][0]!.target![0] + : WorkflowStateMachine.getStateNodeById(Workflow.stateIdFromName(stateName)), + productId, + config, + users + ) + ]; + while (ret.at(-1)!.DestinationState !== WorkflowState.Published) { + ret.push( + Workflow.transitionFromState( + Workflow.nodeFromName(ret.at(-1)!.DestinationState!), + productId, + config, + users + ) + ); + } + return ret; + } + + /** + * Update or create product transition + */ + private async updateProductTransitions( + userId: number | null, + initialState: string, + destinationState: string, + command?: string, + comment?: string + ) { + const transition = await prisma.productTransitions.findFirst({ + where: { + ProductId: this.productId, + InitialState: initialState, + DestinationState: destinationState, + DateTransition: null + }, + select: { + Id: true + } + }); + + const user = userId + ? await prisma.users.findUnique({ + where: { + Id: userId + }, + select: { + Name: true, + WorkflowUserId: true + } + }) + : null; + + if (transition) { + await DatabaseWrites.productTransitions.update({ + where: { + Id: transition.Id + }, + data: { + WorkflowUserId: user?.WorkflowUserId ?? null, + AllowedUserNames: user?.Name ?? null, + Command: command ?? null, + DateTransition: new Date(), + Comment: comment ?? null + } + }); + } else { + await DatabaseWrites.productTransitions.create({ + data: { + ProductId: this.productId, + WorkflowUserId: user?.WorkflowUserId ?? null, + AllowedUserNames: user?.Name ?? null, + InitialState: initialState, + DestinationState: destinationState, + Command: command ?? null, + DateTransition: new Date(), + Comment: comment ?? null, + WorkflowType: this.config.workflowType + } + }); + } + } + + private static stateName(s: XStateNode): string { + return s.id.replace(WorkflowStateMachine.id + '.', ''); + } + + private static stateIdFromName(s: string): string { + return WorkflowStateMachine.id + '.' + s; + } + + private static nodeFromName(s: string): XStateNode { + return WorkflowStateMachine.getStateNodeById(Workflow.stateIdFromName(s)); + } + + private static targetStringFromEvent( + e: TransitionDefinition + ): string { + return ( + e + .toJSON() + .target?.at(0) + ?.replace('#' + WorkflowStateMachine.id + '.', '') || '' + ); + } + + private async checkAuthorsAndReviewers() { + return ( + await prisma.projects.findMany({ + where: { + Products: { + some: { + Id: this.productId + } + } + }, + select: { + _count: { + select: { + Authors: true, + Reviewers: true + } + } + } + }) + )[0]; + } +} diff --git a/source/SIL.AppBuilder.Portal/common/workflow/state-machine.ts b/source/SIL.AppBuilder.Portal/common/workflow/state-machine.ts new file mode 100644 index 0000000000..6f7290d068 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/workflow/state-machine.ts @@ -0,0 +1,885 @@ +import { assign, setup } from 'xstate'; +import { BullMQ, Queues, Workflow } from '../index.js'; +import { RoleId, WorkflowType } from '../public/prisma.js'; +import type { + WorkflowContext, + WorkflowEvent, + WorkflowInput, + WorkflowStateMeta, + WorkflowTransitionMeta +} from '../public/workflow.js'; +import { + ActionType, + ENVKeys, + ProductType, + WorkflowAction, + WorkflowOptions, + WorkflowState, + hasAuthors, + hasReviewers, + jump +} from '../public/workflow.js'; + +/** + * IMPORTANT: READ THIS BEFORE EDITING A STATE MACHINE! + * + * Conventions: + * - Each state must have a `meta` with the appropriate properties if it should be included only under certain conditions + * - Each state must have an `entry` in which: + * - All relevant context variables are `assign`ed (e.g. `instructions`, `includeFields`) + * - If `includeReviewers` or `includeFields` is set in `entry`, there must be a corresponding `exit` that `assign`s them to `false` + * - the `snapAndTasks` action is called with the appropriate parameters + * - Each transition must use the `transit` action with the appropriate parameters + * - Each transition must have a `meta` that specifies who can initiate the transition + * - The `meta` may also specify filtering criteria like in a state meta + * - The first transition in a state will be the "happy" path, assuming the state is in the "happy" path + */ +export const WorkflowStateMachine = setup({ + types: { + context: {} as WorkflowContext, + input: {} as WorkflowInput, + meta: {} as WorkflowStateMeta | WorkflowTransitionMeta, + events: {} as WorkflowEvent + } +}).createMachine({ + id: 'WorkflowDefinition', + initial: WorkflowState.Start, + context: ({ input }) => ({ + instructions: null, + /** projectName and projectDescription are always included */ + includeFields: [], + /** Reset to false on exit */ + includeReviewers: false, + /** Reset to false on exit */ + includeArtifacts: false, + environment: {}, + workflowType: input.workflowType, + productType: input.productType, + options: input.options, + productId: input.productId, + hasAuthors: input.hasAuthors, + hasReviewers: input.hasReviewers + }), + states: { + [WorkflowState.Start]: { + always: [ + jump({ + target: WorkflowState.Approval, + filter: { + options: { has: WorkflowOptions.ApprovalProcess }, + workflowType: { is: WorkflowType.Startup } + } + }), + jump({ + target: WorkflowState.Approval_Pending, + filter: { + options: { has: WorkflowOptions.ApprovalProcess }, + workflowType: { is: WorkflowType.Startup } + } + }), + jump({ + target: WorkflowState.Terminated, + filter: { + options: { has: WorkflowOptions.ApprovalProcess }, + workflowType: { is: WorkflowType.Startup } + } + }), + jump({ + target: WorkflowState.Product_Creation, + filter: { + workflowType: { is: WorkflowType.Startup } + } + }), + jump({ + target: WorkflowState.App_Builder_Configuration, + filter: { + workflowType: { is: WorkflowType.Startup } + } + }), + jump( + { + target: WorkflowState.Author_Configuration, + filter: { + options: { has: WorkflowOptions.AllowTransferToAuthors }, + workflowType: { is: WorkflowType.Startup } + } + }, + [hasAuthors] + ), + jump({ target: WorkflowState.Synchronize_Data }), + jump( + { + target: WorkflowState.Author_Download, + filter: { options: { has: WorkflowOptions.AllowTransferToAuthors } } + }, + [hasAuthors] + ), + //note: authors can upload at any time, this state is just to prompt an upload + jump( + { + target: WorkflowState.Author_Upload, + filter: { options: { has: WorkflowOptions.AllowTransferToAuthors } } + }, + [hasAuthors] + ), + jump({ target: WorkflowState.Product_Build }), + jump({ + target: WorkflowState.App_Store_Preview, + filter: { + productType: { is: ProductType.Android_GooglePlay }, + workflowType: { is: WorkflowType.Startup } + } + }), + jump({ + target: WorkflowState.Create_App_Store_Entry, + filter: { + productType: { is: ProductType.Android_GooglePlay }, + workflowType: { is: WorkflowType.Startup } + } + }), + jump({ target: WorkflowState.Verify_and_Publish }), + jump({ target: WorkflowState.Product_Publish }), + jump({ + target: WorkflowState.Make_It_Live, + filter: { + productType: { is: ProductType.Android_GooglePlay }, + workflowType: { is: WorkflowType.Startup } + } + }), + jump({ target: WorkflowState.Published }), + { + guard: ({ context }) => + context.options.has(WorkflowOptions.ApprovalProcess) && + context.workflowType === WorkflowType.Startup, + target: WorkflowState.Approval + }, + { + guard: ({ context }) => context.workflowType === WorkflowType.Startup, + target: WorkflowState.Product_Creation + }, + { + guard: ({ context }) => context.workflowType !== WorkflowType.Startup, + target: WorkflowState.Product_Build + } + ], + on: { + // this is here just so the default start transition shows up in the visualization + // don't actually use this transition + [WorkflowAction.Default]: [ + { + meta: { + type: ActionType.Auto, + includeWhen: { + options: { has: WorkflowOptions.ApprovalProcess }, + workflowType: { is: WorkflowType.Startup } + } + }, + target: WorkflowState.Approval + }, + { + meta: { + type: ActionType.Auto, + includeWhen: { + workflowType: { is: WorkflowType.Startup } + } + }, + target: WorkflowState.Product_Creation + }, + { + meta: { + type: ActionType.Auto, + includeWhen: { + workflowType: { not: WorkflowType.Startup } + } + }, + target: WorkflowState.Product_Build + } + ] + } + }, + [WorkflowState.Approval]: { + meta: { + includeWhen: { + options: { has: WorkflowOptions.ApprovalProcess }, + workflowType: { is: WorkflowType.Startup } + } + }, + entry: assign({ + instructions: null, + includeFields: [ + 'ownerName', + 'ownerEmail', + 'storeDescription', + 'listingLanguageCode', + 'productDescription', + 'appType', + 'projectLanguageCode' + ] + }), + on: { + [WorkflowAction.Approve]: { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin + }, + target: WorkflowState.Product_Creation + }, + [WorkflowAction.Hold]: { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin + }, + target: WorkflowState.Approval_Pending + }, + [WorkflowAction.Reject]: { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin + }, + target: WorkflowState.Terminated + } + } + }, + [WorkflowState.Approval_Pending]: { + meta: { + includeWhen: { + options: { has: WorkflowOptions.ApprovalProcess }, + workflowType: { is: WorkflowType.Startup } + } + }, + entry: [ + assign({ + instructions: 'approval_pending', + includeFields: ['storeDescription', 'listingLanguageCode'] + }) + ], + on: { + [WorkflowAction.Reject]: { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin + }, + target: WorkflowState.Terminated + }, + [WorkflowAction.Hold]: { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin + } + }, + [WorkflowAction.Approve]: { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin + }, + target: WorkflowState.Product_Creation + } + } + }, + [WorkflowState.Terminated]: { + meta: { + includeWhen: { + options: { has: WorkflowOptions.ApprovalProcess }, + workflowType: { is: WorkflowType.Startup } + } + }, + entry: ({ context }) => Workflow.delete(context.productId), + type: 'final' + }, + [WorkflowState.Product_Creation]: { + meta: { + includeWhen: { + workflowType: { is: WorkflowType.Startup } + } + }, + entry: [ + assign({ instructions: 'waiting' }), + ({ context }) => { + Queues.Miscellaneous.add( + `Create Product #${context.productId}`, + { + type: BullMQ.JobType.Product_Create, + productId: context.productId + }, + BullMQ.Retry5e5 + ); + } + ], + on: { + [WorkflowAction.Product_Created]: { + meta: { type: ActionType.Auto }, + target: WorkflowState.App_Builder_Configuration + } + } + }, + [WorkflowState.App_Builder_Configuration]: { + meta: { + includeWhen: { + workflowType: { is: WorkflowType.Startup } + } + }, + entry: assign({ + instructions: ({ context }) => + context.productType === ProductType.Android_GooglePlay + ? 'googleplay_configuration' + : 'app_configuration', + includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'] + }), + on: { + [WorkflowAction.New_App]: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + includeWhen: { + productType: { is: ProductType.Android_GooglePlay } + } + }, + target: WorkflowState.Product_Build + }, + [WorkflowAction.Continue]: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + includeWhen: { + productType: { not: ProductType.Android_GooglePlay } + } + }, + target: WorkflowState.Product_Build + }, + [WorkflowAction.Existing_App]: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + includeWhen: { + productType: { is: ProductType.Android_GooglePlay } + } + }, + actions: assign({ + environment: ({ context }) => ({ + ...context.environment, + [ENVKeys.GOOGLE_PLAY_EXISTING]: '1' + }) + }), + target: WorkflowState.Product_Build + }, + [WorkflowAction.Transfer_to_Authors]: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + includeWhen: { + options: { has: WorkflowOptions.AllowTransferToAuthors } + } + }, + guard: hasAuthors, + target: WorkflowState.Author_Configuration + } + } + }, + [WorkflowState.Author_Configuration]: { + meta: { + includeWhen: { + options: { has: WorkflowOptions.AllowTransferToAuthors }, + workflowType: { is: WorkflowType.Startup } + } + }, + entry: assign({ + instructions: 'app_configuration', + includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'] + }), + on: { + [WorkflowAction.Continue]: { + meta: { + type: ActionType.User, + user: RoleId.Author + }, + target: WorkflowState.App_Builder_Configuration + }, + [WorkflowAction.Take_Back]: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + target: WorkflowState.App_Builder_Configuration + } + } + }, + [WorkflowState.Synchronize_Data]: { + entry: assign({ + instructions: 'synchronize_data', + includeFields: ['storeDescription', 'listingLanguageCode'], + includeArtifacts: true + }), + exit: assign({ + includeArtifacts: false + }), + on: { + [WorkflowAction.Continue]: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + target: WorkflowState.Product_Build + }, + [WorkflowAction.Transfer_to_Authors]: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + includeWhen: { + options: { has: WorkflowOptions.AllowTransferToAuthors } + } + }, + guard: hasAuthors, + target: WorkflowState.Author_Download + } + } + }, + [WorkflowState.Author_Download]: { + meta: { + includeWhen: { + options: { has: WorkflowOptions.AllowTransferToAuthors } + } + }, + entry: assign({ + instructions: 'authors_download', + includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'] + }), + on: { + [WorkflowAction.Continue]: { + meta: { + type: ActionType.User, + user: RoleId.Author + }, + target: WorkflowState.Author_Upload + }, + [WorkflowAction.Take_Back]: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + target: WorkflowState.Synchronize_Data + } + } + }, + [WorkflowState.Author_Upload]: { + meta: { + includeWhen: { + options: { has: WorkflowOptions.AllowTransferToAuthors } + } + }, + entry: assign({ + instructions: 'authors_upload', + includeFields: ['storeDescription', 'listingLanguageCode'] + }), + on: { + [WorkflowAction.Continue]: { + meta: { + type: ActionType.User, + user: RoleId.Author + }, + target: WorkflowState.Synchronize_Data + }, + [WorkflowAction.Take_Back]: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + target: WorkflowState.Synchronize_Data + } + } + }, + [WorkflowState.Product_Build]: { + entry: [ + assign({ + instructions: 'waiting' + }), + ({ context }) => { + Queues.Builds.add( + `Build Product #${context.productId}`, + { + type: BullMQ.JobType.Build_Product, + productId: context.productId, + defaultTargets: + context.workflowType === WorkflowType.Republish + ? 'play-listing' + : context.productType === ProductType.Android_S3 + ? 'apk' + : context.productType === ProductType.AssetPackage + ? 'asset-package' + : context.productType === ProductType.Web + ? 'html' + : //ProductType.Android_GooglePlay + //default + 'apk play-listing', + // extra env handled in getWorkflowParameters + environment: context.environment + }, + BullMQ.Retry5e5 + ); + } + ], + on: { + [WorkflowAction.Build_Successful]: [ + { + meta: { + type: ActionType.Auto, + includeWhen: { + productType: { is: ProductType.Android_GooglePlay }, + workflowType: { is: WorkflowType.Startup } + } + }, + guard: ({ context }) => + context.productType === ProductType.Android_GooglePlay && + !context.environment[ENVKeys.PUBLISH_GOOGLE_PLAY_UPLOADED_BUILD_ID] && + context.workflowType === WorkflowType.Startup, + target: WorkflowState.App_Store_Preview + }, + { + meta: { type: ActionType.Auto }, + guard: ({ context }) => + context.productType !== ProductType.Android_GooglePlay || + context.environment[ENVKeys.GOOGLE_PLAY_EXISTING] === '1' || + !!context.environment[ENVKeys.PUBLISH_GOOGLE_PLAY_UPLOADED_BUILD_ID], + target: WorkflowState.Verify_and_Publish + } + ], + [WorkflowAction.Build_Failed]: { + meta: { type: ActionType.Auto }, + target: WorkflowState.Synchronize_Data + } + } + }, + [WorkflowState.App_Store_Preview]: { + meta: { + includeWhen: { + productType: { is: ProductType.Android_GooglePlay }, + workflowType: { is: WorkflowType.Startup } + } + }, + entry: assign({ + instructions: null, + includeFields: [ + 'ownerName', + 'ownerEmail', + 'storeDescription', + 'listingLanguageCode', + 'productDescription', + 'appType', + 'projectLanguageCode' + ], + includeArtifacts: true + }), + exit: assign({ includeArtifacts: false }), + on: { + [WorkflowAction.Approve]: [ + { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin, + includeWhen: { + options: { + has: WorkflowOptions.AdminStoreAccess + } + } + }, + target: WorkflowState.Create_App_Store_Entry + }, + { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + includeWhen: { + options: { none: new Set([WorkflowOptions.AdminStoreAccess]) } + } + }, + target: WorkflowState.Create_App_Store_Entry + } + ], + [WorkflowAction.Reject]: [ + { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin, + includeWhen: { + options: { has: WorkflowOptions.AdminStoreAccess } + } + }, + target: WorkflowState.Synchronize_Data + }, + { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + includeWhen: { + options: { none: new Set([WorkflowOptions.AdminStoreAccess]) } + } + }, + target: WorkflowState.Synchronize_Data + } + ] + } + }, + [WorkflowState.Create_App_Store_Entry]: { + meta: { + includeWhen: { + productType: { is: ProductType.Android_GooglePlay }, + workflowType: { is: WorkflowType.Startup } + } + }, + entry: assign({ + instructions: 'create_app_entry', + includeFields: ['storeDescription', 'listingLanguageCode'], + includeArtifacts: true, + environment: ({ context }) => ({ + ...context.environment, + [ENVKeys.GOOGLE_PLAY_DRAFT]: '1' + }) + }), + exit: assign({ includeArtifacts: false }), + on: { + [WorkflowAction.Continue]: [ + { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin, + includeWhen: { + options: { has: WorkflowOptions.AdminStoreAccess } + } + }, + actions: ({ context }) => { + // Given that the Set Google Play Uploaded action in S1 require DB and BuildEngine queries, this is probably the best way to do this + Queues.Miscellaneous.add(`Get VersionCode for Product #${context.productId}`, { + type: BullMQ.JobType.Product_GetVersionCode, + productId: context.productId + }); + }, + target: WorkflowState.Verify_and_Publish + }, + { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + includeWhen: { + options: { none: new Set([WorkflowOptions.AdminStoreAccess]) } + } + }, + actions: ({ context }) => { + Queues.Miscellaneous.add(`Get VersionCode for Product #${context.productId}`, { + type: BullMQ.JobType.Product_GetVersionCode, + productId: context.productId + }); + }, + target: WorkflowState.Verify_and_Publish + } + ], + [WorkflowAction.Reject]: [ + { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin, + includeWhen: { + options: { has: WorkflowOptions.AdminStoreAccess } + } + }, + target: WorkflowState.Synchronize_Data + }, + { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + includeWhen: { + options: { none: new Set([WorkflowOptions.AdminStoreAccess]) } + } + }, + target: WorkflowState.Synchronize_Data + } + ] + } + }, + [WorkflowState.Verify_and_Publish]: { + entry: assign({ + instructions: ({ context }) => { + switch (context.productType) { + case ProductType.Android_GooglePlay: + return 'googleplay_verify_and_publish'; + case ProductType.Android_S3: + return 'verify_and_publish'; + case ProductType.AssetPackage: + return 'asset_package_verify_and_publish'; + case ProductType.Web: + return 'web_verify'; + } + }, + includeFields: ({ context }) => { + switch (context.productType) { + case ProductType.Android_GooglePlay: + case ProductType.Android_S3: + return ['storeDescription', 'listingLanguageCode']; + case ProductType.AssetPackage: + case ProductType.Web: + return ['storeDescription']; + } + }, + includeReviewers: true, + includeArtifacts: true + }), + exit: assign({ + includeReviewers: false, + includeArtifacts: false + }), + on: { + [WorkflowAction.Approve]: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + target: WorkflowState.Product_Publish + }, + [WorkflowAction.Reject]: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + target: WorkflowState.Synchronize_Data + }, + [WorkflowAction.Email_Reviewers]: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + guard: hasReviewers, + actions: () => { + // TODO: connect to backend to email reviewers + console.log('Emailing Reviewers'); + } + } + } + }, + [WorkflowState.Product_Publish]: { + entry: [ + assign({ instructions: 'waiting' }), + ({ context }) => { + Queues.Publishing.add( + `Publish Product #${context.productId}`, + { + type: BullMQ.JobType.Publish_Product, + productId: context.productId, + defaultChannel: 'production', //default unless overriden by WorkflowDefinition.Properties or ProductDefinition.Properties + defaultTargets: + context.productType === ProductType.Android_GooglePlay + ? 'google-play' + : context.productType === ProductType.Web + ? 'rclone' + : //ProductType.Android_S3 + //ProductType.AssetPackage + //default + 's3-bucket', + environment: context.environment + }, + BullMQ.Retry5e5 + ); + } + ], + on: { + [WorkflowAction.Publish_Completed]: [ + { + meta: { + type: ActionType.Auto, + includeWhen: { + productType: { is: ProductType.Android_GooglePlay }, + workflowType: { is: WorkflowType.Startup } + } + }, + guard: ({ context }) => + context.productType === ProductType.Android_GooglePlay && + !context.environment[ENVKeys.GOOGLE_PLAY_EXISTING] && + context.workflowType === WorkflowType.Startup, + target: WorkflowState.Make_It_Live + }, + { + meta: { type: ActionType.Auto }, + guard: ({ context }) => + context.productType !== ProductType.Android_GooglePlay || + context.environment[ENVKeys.GOOGLE_PLAY_EXISTING] === '1', + target: WorkflowState.Published + } + ], + [WorkflowAction.Publish_Failed]: { + meta: { type: ActionType.Auto }, + target: WorkflowState.Synchronize_Data + } + } + }, + [WorkflowState.Make_It_Live]: { + meta: { + includeWhen: { + productType: { is: ProductType.Android_GooglePlay }, + workflowType: { is: WorkflowType.Startup } + } + }, + entry: assign({ + instructions: 'make_it_live', + includeFields: ['storeDescription', 'listingLanguageCode'] + }), + on: { + [WorkflowAction.Continue]: [ + { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin, + includeWhen: { + options: { has: WorkflowOptions.AdminStoreAccess } + } + }, + target: WorkflowState.Published + }, + { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + includeWhen: { + options: { none: new Set([WorkflowOptions.AdminStoreAccess]) } + } + }, + target: WorkflowState.Published + } + ], + [WorkflowAction.Reject]: [ + { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin, + includeWhen: { + options: { has: WorkflowOptions.AdminStoreAccess } + } + }, + target: WorkflowState.Synchronize_Data + }, + { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + includeWhen: { + options: { none: new Set([WorkflowOptions.AdminStoreAccess]) } + } + }, + target: WorkflowState.Synchronize_Data + } + ] + } + }, + [WorkflowState.Published]: { + entry: ({ context }) => Workflow.delete(context.productId), + type: 'final' + } + }, + on: { + [WorkflowAction.Jump]: { + actions: [ + assign({ + start: ({ event }) => event.target + }) + ], + target: `.${WorkflowState.Start}` + } + } +}); diff --git a/source/SIL.AppBuilder.Portal/node-server/BullWorker.ts b/source/SIL.AppBuilder.Portal/node-server/BullWorker.ts new file mode 100644 index 0000000000..86c989bfa1 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/BullWorker.ts @@ -0,0 +1,115 @@ +import { Job, Worker } from 'bullmq'; +import { BullMQ, Queues } from 'sil.appbuilder.portal.common'; +import * as Executor from './job-executors/index.js'; + +export abstract class BullWorker { + public worker: Worker; + constructor(public queue: BullMQ.QueueName) { + this.worker = new Worker(queue, this.run, { + connection: { + host: process.env.NODE_ENV === 'development' ? 'localhost' : 'redis' + } + }); + } + abstract run(job: Job): Promise; +} + +export class Builds extends BullWorker { + constructor() { + super(BullMQ.QueueName.Builds); + } + async run(job: Job) { + switch (job.data.type) { + case BullMQ.JobType.Build_Product: + return Executor.Build.product(job as Job); + case BullMQ.JobType.Build_PostProcess: + return Executor.Build.postProcess(job as Job); + } + } +} + +export class DefaultRecurring extends BullWorker { + constructor() { + super(BullMQ.QueueName.DefaultRecurring); + Queues.DefaultRecurring.add( + 'Check System Statuses (Recurring)', + { + type: BullMQ.JobType.System_CheckStatuses + }, + { + repeat: { + pattern: '*/5 * * * *', // every 5 minutes + key: 'defaultCheckSystemStatuses' + } + } + ); + } + async run(job: Job) { + switch (job.data.type) { + case BullMQ.JobType.System_CheckStatuses: + return Executor.System.checkStatuses(job as Job); + } + } +} + +export class Miscellaneous extends BullWorker { + constructor() { + super(BullMQ.QueueName.Miscellaneous); + } + async run(job: Job) { + switch (job.data.type) { + case BullMQ.JobType.Product_Create: + return Executor.Product.create(job as Job); + case BullMQ.JobType.Product_Delete: + return Executor.Product.deleteProduct(job as Job); + case BullMQ.JobType.Product_GetVersionCode: + return Executor.Product.getVersionCode(job as Job); + case BullMQ.JobType.Project_Create: + return Executor.Project.create(job as Job); + case BullMQ.JobType.Project_ImportProducts: + return Executor.Project.importProducts(job as Job); + } + } +} + +export class Publishing extends BullWorker { + constructor() { + super(BullMQ.QueueName.Publishing); + } + async run(job: Job) { + switch (job.data.type) { + case BullMQ.JobType.Publish_Product: + return Executor.Publish.product(job as Job); + case BullMQ.JobType.Publish_PostProcess: + return Executor.Publish.postProcess(job as Job); + } + } +} + +export class RemotePolling extends BullWorker { + constructor() { + super(BullMQ.QueueName.RemotePolling); + } + async run(job: Job) { + switch (job.data.type) { + case BullMQ.JobType.Build_Check: + return Executor.Build.check(job as Job); + case BullMQ.JobType.Publish_Check: + return Executor.Publish.check(job as Job); + case BullMQ.JobType.Project_Check: + return Executor.Project.check(job as Job); + } + } +} + +export class UserTasks extends BullWorker { + constructor() { + super(BullMQ.QueueName.UserTasks); + } + async run(job: Job) { + switch (job.data.type) { + case BullMQ.JobType.UserTasks_Modify: + return Executor.UserTasks.modify(job as Job); + } + } +} diff --git a/source/SIL.AppBuilder.Portal/node-server/dev.ts b/source/SIL.AppBuilder.Portal/node-server/dev.ts new file mode 100644 index 0000000000..2bc04249fe --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/dev.ts @@ -0,0 +1,26 @@ +import { createBullBoard } from '@bull-board/api'; +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js'; +import { ExpressAdapter } from '@bull-board/express'; +import express from 'express'; +import * as Workers from './BullWorker.js'; + +process.env.NODE_ENV = 'development'; + +const app = express(); + +import { Queues } from 'sil.appbuilder.portal.common'; +const serverAdapter = new ExpressAdapter(); +serverAdapter.setBasePath('/'); +createBullBoard({ + queues: Object.values(Queues).map((q) => new BullMQAdapter(q)), + serverAdapter +}); +app.use(serverAdapter.getRouter()); +app.listen(3000, () => console.log('Dev server started')); + +new Workers.Builds(); +new Workers.DefaultRecurring(); +new Workers.Miscellaneous(); +new Workers.Publishing(); +new Workers.RemotePolling(); +new Workers.UserTasks(); diff --git a/source/SIL.AppBuilder.Portal/node-server/index.ts b/source/SIL.AppBuilder.Portal/node-server/index.ts new file mode 100644 index 0000000000..f78a4d6f2d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/index.ts @@ -0,0 +1,102 @@ +import { getSession, type ExpressAuthConfig } from '@auth/express'; +import Auth0Provider from '@auth/sveltekit/providers/auth0'; +import { createBullBoard } from '@bull-board/api'; +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js'; +import { ExpressAdapter } from '@bull-board/express'; +import express, { type NextFunction, type Request, type Response } from 'express'; +import path from 'path'; +import { prisma, Queues } from 'sil.appbuilder.portal.common'; +import { fileURLToPath } from 'url'; +import * as Workers from './BullWorker.js'; + +// Do not import any functional code from the sveltekit codebase +// unless you are positive you know what you are doing + +const app = express(); + +// Workaround for prisma expecting CommonJS (__dirname and __filename polyfill) +// Polyfill https://github.com/prisma/prisma/issues/15614#issuecomment-2126271831 +globalThis.__filename = fileURLToPath(import.meta.url); +globalThis.__dirname = path.dirname(__filename); + +const authConfig: ExpressAuthConfig = { + secret: process.env.VITE_AUTH0_SECRET, + providers: [ + Auth0Provider({ + id: 'auth0', + name: 'Auth0', + clientId: process.env.VITE_AUTH0_CLIENT_ID, + clientSecret: process.env.VITE_AUTH0_CLIENT_SECRET, + issuer: `https://${process.env.VITE_AUTH0_DOMAIN}/`, + wellKnown: `https://${process.env.VITE_AUTH0_DOMAIN}/.well-known/openid-configuration` + }) + ], + callbacks: { + async session({ session, token }) { + // @ts-expect-error userId is not defined in source + session.user.userId = token.userId; + return session; + } + } +}; + +function verifyAuthenticated(requireAdmin: boolean) { + return async (req: Request, res: Response, next: NextFunction) => { + const session = res.locals.session ?? (await getSession(req, authConfig)); + if (!session?.user) { + res.redirect('/login'); + } else if (requireAdmin) { + const superAdminRoles = await prisma.userRoles.findMany({ + where: { + UserId: session.user.userId, + // RoleId.SuperAdmin + RoleId: 1 + } + }); + if (superAdminRoles.length === 0) { + // Could redirect to /tasks + res.status(403); + res.end('You do not have permission to access this resource'); + } else { + next(); + } + } else { + next(); + } + }; +} + +// No auth +app.get('/healthcheck', (req, res) => { + res.end('ok'); +}); + +// BullMQ variables +// Running on svelte process right now. Consider putting on new thread +// Fine like this if majority of job time is waiting for network requests +// If there is much processing it should be moved to another thread +new Workers.Builds(); +new Workers.DefaultRecurring(); +new Workers.Miscellaneous(); +new Workers.Publishing(); +new Workers.RemotePolling(); +new Workers.UserTasks(); + +const serverAdapter = new ExpressAdapter(); +serverAdapter.setBasePath('/admin/jobs'); +createBullBoard({ + queues: Object.values(Queues).map((q) => new BullMQAdapter(q)), + serverAdapter +}); + +// Require admin auth +app.use('/admin/jobs', verifyAuthenticated(true), serverAdapter.getRouter()); + +// build folder does not exist normally, but does during build +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +const handler = await import('./build/handler.js'); +// Svelte application handles authentication already, including login and logout +app.use(handler.handler); + +app.listen(3000, () => console.log('Server started!')); diff --git a/source/SIL.AppBuilder.Portal/node-server/job-executors/build.ts b/source/SIL.AppBuilder.Portal/node-server/job-executors/build.ts new file mode 100644 index 0000000000..8ee9013e91 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/job-executors/build.ts @@ -0,0 +1,263 @@ +import { Job } from 'bullmq'; +import { + BuildEngine, + BullMQ, + DatabaseWrites, + prisma, + Queues, + Workflow +} from 'sil.appbuilder.portal.common'; +import { WorkflowAction } from 'sil.appbuilder.portal.common/workflow'; +import { + addProductPropertiesToEnvironment, + getWorkflowParameters +} from './common.build-publish.js'; + +export async function product(job: Job): Promise { + const productData = await prisma.products.findUnique({ + where: { + Id: job.data.productId + }, + select: { + Project: { + select: { + OrganizationId: true + } + }, + WorkflowJobId: true, + WorkflowInstance: { + select: { + Id: true + } + } + } + }); + if (!productData) { + throw new Error(`Product #${job.data.productId} does not exist!`); + } + job.updateProgress(10); + if (productData.WorkflowInstance) { + // reset previous build + await DatabaseWrites.products.update(job.data.productId, { + WorkflowBuildId: 0 + }); + job.updateProgress(20); + const params = await getWorkflowParameters(productData.WorkflowInstance.Id, 'build'); + job.updateProgress(30); + const env = await addProductPropertiesToEnvironment(job.data.productId); + job.updateProgress(40); + const response = await BuildEngine.Requests.createBuild( + { type: 'query', organizationId: productData.Project.OrganizationId }, + productData.WorkflowJobId, + { + targets: params['targets'] ?? job.data.defaultTargets, + environment: { ...env, ...params.environment, ...job.data.environment } + } + ); + job.updateProgress(50); + if (response.responseType === 'error') { + const flow = await Workflow.restore(job.data.productId); + // TODO: Send Notification of Failure + flow?.send({ type: WorkflowAction.Build_Failed, userId: null, comment: response.message }); + } else { + await DatabaseWrites.products.update(job.data.productId, { + WorkflowBuildId: response.id + }); + job.updateProgress(65); + + const productBuild = await DatabaseWrites.productBuilds.create({ + data: { + ProductId: job.data.productId, + BuildId: response.id + } + }); + + job.updateProgress(85); + + await Queues.RemotePolling.add( + `Check status of Build #${response.id}`, + { + type: BullMQ.JobType.Build_Check, + productId: job.data.productId, + organizationId: productData.Project.OrganizationId, + jobId: productData.WorkflowJobId, + buildId: response.id, + productBuildId: productBuild.Id + }, + BullMQ.RepeatEveryMinute + ); + } + job.updateProgress(100); + return { + response: { + ...response, + environment: + response.responseType !== 'error' + ? JSON.parse(response['environment'] ?? '{}') + : undefined + }, + params, + env + }; + } + else { + job.log('No WorkflowInstance found. Workflow cancelled?'); + job.updateProgress(100); + return { productData }; + } +} + +export async function check(job: Job): Promise { + const product = await prisma.products.findFirst({ + where: { + WorkflowJobId: job.data.jobId, + WorkflowBuildId: job.data.buildId + }, + select: { + WorkflowInstance: { + select: { + Id: true + } + } + } + }); + if (!product?.WorkflowInstance) { + await Queues.RemotePolling.removeRepeatableByKey(job.repeatJobKey); + job.log('No WorkflowInstance found. Workflow cancelled?'); + job.updateProgress(100); + return { product }; + } + job.updateProgress(25); + const response = await BuildEngine.Requests.getBuild( + { type: 'query', organizationId: job.data.organizationId }, + job.data.jobId, + job.data.buildId + ); + job.updateProgress(50); + if (response.responseType === 'error') { + throw new Error(response.message); + } else { + if (response.status === 'completed') { + await Queues.RemotePolling.removeRepeatableByKey(job.repeatJobKey); + await Queues.Builds.add( + `PostProcess Build #${job.data.buildId} for Product #${job.data.productId}`, + { + type: BullMQ.JobType.Build_PostProcess, + productId: job.data.productId, + productBuildId: job.data.productBuildId, + build: response + } + ); + } + job.updateProgress(100); + return { + ...response, + environment: JSON.parse(response['environment'] ?? '{}') + }; + } +} + +export async function postProcess(job: Job): Promise { + if (job.data.build.error) { + job.log(job.data.build.error); + } + let latestArtifactDate = new Date(0); + job.log('ARTIFACTS:'); + const artifacts = await DatabaseWrites.productArtifacts.createManyAndReturn({ + data: await Promise.all( + Object.entries(job.data.build.artifacts).map(async ([type, url]) => { + job.log(`${type}: ${url}`); + const res = await fetch(url, { method: 'HEAD' }); + const lastModified = new Date(res.headers.get('Last-Modified')); + if (lastModified > latestArtifactDate) { + latestArtifactDate = lastModified; + } + + // On version.json, update the ProductBuild.Version + if (type === 'version' && res.headers.get('Content-Type') === 'application/json') { + const version = JSON.parse(await fetch(url).then((r) => r.text())); + if (version['version']) { + await DatabaseWrites.productBuilds.update({ + where: { + Id: job.data.productBuildId + }, + data: { + Version: version['version'] + } + }); + if (job.data.build.result === 'SUCCESS') { + await DatabaseWrites.products.update(job.data.productId, { + VersionBuilt: version['version'] + }); + } + } + } + + // On play-listing-manifest.json, update the Project.DefaultLanguage + if ( + type == 'play-listing-manifest' && + res.headers.get('Content-Type') === 'application/json' + ) { + const manifest = JSON.parse(await fetch(url).then((r) => r.text())); + if (manifest['default-language']) { + const lang = await prisma.storeLanguages.findFirst({ + where: { + Name: manifest['default-language'] + }, + select: { + Id: true + } + }); + if (lang !== null) { + await DatabaseWrites.products.update(job.data.productId, { + StoreLanguageId: lang.Id + }); + } + } + } + + return { + ProductId: job.data.productId, + ProductBuildId: job.data.productBuildId, + ArtifactType: type, + Url: url, + ContentType: res.headers.get('Content-Type'), + FileSize: + res.headers.get('Content-Type') !== 'text/html' + ? parseInt(res.headers.get('Content-Length')) + : undefined + }; + }) + ) + }); + await DatabaseWrites.products.update(job.data.productId, { + DateBuilt: latestArtifactDate + }); + job.updateProgress(75); + await DatabaseWrites.productBuilds.update({ + where: { + Id: job.data.productBuildId + }, + data: { + Success: job.data.build.result === 'SUCCESS' + } + }); + job.updateProgress(90); + const flow = await Workflow.restore(job.data.productId); + if (flow) { + if (job.data.build.result === 'SUCCESS') { + flow.send({ type: WorkflowAction.Build_Successful, userId: null }); + } else { + flow.send({ + type: WorkflowAction.Build_Failed, + userId: null, + comment: `system.build-failed,${job.data.build.artifacts['consoleText'] ?? ''}` + }); + } + } + job.updateProgress(100); + return { + created: artifacts.length, + artifacts: artifacts.map((a) => ({ ...a, FileSize: a.FileSize?.toString() })) + }; +} diff --git a/source/SIL.AppBuilder.Portal/node-server/job-executors/common.build-publish.ts b/source/SIL.AppBuilder.Portal/node-server/job-executors/common.build-publish.ts new file mode 100644 index 0000000000..c2f5976a54 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/job-executors/common.build-publish.ts @@ -0,0 +1,156 @@ +import { prisma } from 'sil.appbuilder.portal.common'; +import { WorkflowType, WorkflowTypeString } from 'sil.appbuilder.portal.common/prisma'; +import { Environment, ENVKeys, ProductType } from 'sil.appbuilder.portal.common/workflow'; + +export async function addProductPropertiesToEnvironment(productId: string) { + const product = await prisma.products.findUnique({ + where: { + Id: productId + }, + select: { + Project: { + select: { + Id: true, + Name: true, + Description: true, + Language: true, + Organization: { + select: { + Name: true + } + }, + Owner: { + select: { + Name: true, + Email: true + } + } + } + }, + Properties: true + } + }); + const uiUrl = process.env.UI_URL || 'http://localhost:5173'; + const projectUrl = uiUrl + '/projects/' + product.Project.Id; + + return { + [ENVKeys.UI_URL]: uiUrl, + [ENVKeys.PRODUCT_ID]: productId, + [ENVKeys.PROJECT_ID]: '' + product.Project.Id, + [ENVKeys.PROJECT_NAME]: product.Project.Name ?? '', + [ENVKeys.PROJECT_DESCRIPTION]: product.Project.Description ?? '', + [ENVKeys.PROJECT_URL]: projectUrl, + [ENVKeys.PROJECT_LANGUAGE]: product.Project.Language ?? '', + [ENVKeys.PROJECT_ORGANIZATION]: product.Project.Organization.Name, + [ENVKeys.PROJECT_OWNER_NAME]: product.Project.Owner.Name, + [ENVKeys.PROJECT_OWNER_EMAIL]: product.Project.Owner.Email, + ...(product.Properties ? JSON.parse(product.Properties).environment ?? {} : {}) + } as Environment; +} + +export async function getWorkflowParameters( + workflowInstanceId: number, + scope: 'build' | 'publish' +) { + const instance = await prisma.workflowInstances.findUnique({ + where: { + Id: workflowInstanceId + }, + select: { + Product: { + select: { + ProductDefinition: { + select: { + Properties: true, + Name: true + } + } + } + }, + WorkflowDefinition: { + select: { + Properties: true, + Type: true, + ProductType: true + } + } + } + }); + let environment: Environment = { + WORKFLOW_TYPE: WorkflowTypeString[instance.WorkflowDefinition.Type], + WORKFLOW_PRODUCT_NAME: instance.Product.ProductDefinition.Name + }; + + if (instance.WorkflowDefinition.ProductType !== ProductType.Web) { + environment['BUILD_MANAGE_VERSION_CODE'] = '1'; + environment['BUILD_MANAGE_VERSION_NAME'] = '1'; + if ( + instance.WorkflowDefinition.Type === WorkflowType.Rebuild || + (instance.WorkflowDefinition.ProductType === ProductType.Android_GooglePlay && + instance.WorkflowDefinition.Type !== WorkflowType.Republish) + ) { + environment['BUILD_SHARE_APP_LINK'] = '1'; + } + } + + const result: { [key: string]: string } = {}; + const scoped: { [key: string]: string } = {}; + Object.entries(JSON.parse(instance.WorkflowDefinition.Properties ?? '{}')).forEach(([k, v]) => { + const strValue = JSON.stringify(v); + let strKey = k; + if (strKey === 'environment') { + // merge environment + environment = { + ...environment, + ...JSON.parse(strValue) + }; + } + // Allow for scoped names so "build:targets" will become "targets" + // Scoped values should be assigned after non-scoped + else if (strKey.includes(':')) { + // Use scoped values for this scope and ignore others + if (scope && strKey.startsWith(scope + ':')) { + strKey = strKey.split(':')[1]; + scoped[strKey] = strValue; + } + } else { + result[strKey] = strValue; + } + }); + Object.entries(JSON.parse(instance.Product.ProductDefinition.Properties ?? '{}')).forEach( + ([k, v]) => { + const strValue = JSON.stringify(v); + let strKey = k; + if (strKey === 'environment') { + // merge environment + environment = { + ...environment, + ...JSON.parse(strValue) + }; + } + // Allow for scoped names so "build:targets" will become "targets" + // Scoped values should be assigned after non-scoped + else if (strKey.includes(':')) { + // Use scoped values for this scope and ignore others + if (scope && strKey.startsWith(scope + ':')) { + strKey = strKey.split(':')[1]; + scoped[strKey] = strValue; + } + } else { + result[strKey] = strValue; + } + } + ); + Object.entries(scoped).forEach(([k, v]) => { + if (k === 'environment') { + environment = { + ...environment, + ...JSON.parse(v) + }; + } else { + result[k] = v; + } + }); + + return { ...result, environment: environment }; +} diff --git a/source/SIL.AppBuilder.Portal/node-server/job-executors/index.ts b/source/SIL.AppBuilder.Portal/node-server/job-executors/index.ts new file mode 100644 index 0000000000..a7a7b61451 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/job-executors/index.ts @@ -0,0 +1,7 @@ +export * as Build from './build.js'; +export * as Product from './product.js'; +export * as Project from './project.js'; +export * as Publish from './publish.js'; +export * as System from './system.js'; +export * as UserTasks from './userTasks.js'; + diff --git a/source/SIL.AppBuilder.Portal/node-server/job-executors/product.ts b/source/SIL.AppBuilder.Portal/node-server/job-executors/product.ts new file mode 100644 index 0000000000..1c418106c6 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/job-executors/product.ts @@ -0,0 +1,164 @@ +import { Job } from 'bullmq'; +import { + BuildEngine, + BullMQ, + DatabaseWrites, + prisma, + Workflow +} from 'sil.appbuilder.portal.common'; +import { + ENVKeys, + WorkflowAction, + WorkflowInstanceContext +} from 'sil.appbuilder.portal.common/workflow'; + +export async function create(job: Job): Promise { + const productData = await prisma.products.findUnique({ + where: { + Id: job.data.productId + }, + select: { + Project: { + select: { + ApplicationType: { + select: { + Name: true + } + }, + WorkflowProjectUrl: true, + OrganizationId: true + } + }, + Store: { + select: { + Name: true + } + } + } + }); + job.updateProgress(25); + const response = await BuildEngine.Requests.createJob( + { type: 'query', organizationId: productData.Project.OrganizationId }, + { + request_id: job.data.productId, + git_url: productData.Project.WorkflowProjectUrl, + app_id: productData.Project.ApplicationType.Name, + publisher_id: productData.Store.Name + } + ); + job.updateProgress(50); + if (response.responseType === 'error') { + // TODO: What do I do here? Wait some period of time and retry? + job.log(response.message); + throw new Error(response.message); + } else { + await DatabaseWrites.products.update(job.data.productId, { + WorkflowJobId: response.id + }); + job.updateProgress(75); + const flow = await Workflow.restore(job.data.productId); + + flow?.send({ type: WorkflowAction.Product_Created, userId: null }); + + job.updateProgress(100); + return response; + } +} + +export async function deleteProduct(job: Job): Promise { + const response = await BuildEngine.Requests.deleteJob( + { type: 'query', organizationId: job.data.organizationId }, + job.data.workflowJobId + ); + job.updateProgress(50); + if (response.responseType === 'error') { + job.log(response.message); + throw new Error(response.message); + } else { + job.updateProgress(100); + return response.status; + } +} + +export async function getVersionCode(job: Job): Promise { + let versionCode = 0; + const product = await prisma.products.findUnique({ + where: { + Id: job.data.productId + }, + select: { + WorkflowBuildId: true, + WorkflowJobId: true, + Project: { + select: { + Organization: { + select: { + BuildEngineUrl: true, + BuildEngineApiAccessToken: true + } + } + } + } + } + }); + job.updateProgress(30); + if (product?.WorkflowBuildId && product?.WorkflowJobId) { + const productBuild = await prisma.productBuilds.findFirst({ + where: { + ProductId: job.data.productId, + BuildId: product.WorkflowBuildId + }, + select: { + Id: true + } + }); + if (!productBuild) { + return 0; + } + job.updateProgress(45); + const versionCodeArtifact = await prisma.productArtifacts.findFirst({ + where: { + ProductId: job.data.productId, + ProductBuildId: productBuild.Id, + ArtifactType: 'version' + }, + select: { + Url: true + } + }); + if (!versionCodeArtifact) { + return 0; + } + job.updateProgress(60); + const version = JSON.parse(await fetch(versionCodeArtifact.Url).then((r) => r.text())); + if (version['versionCode'] !== undefined) { + versionCode = parseInt(version['versionCode']); + } + job.updateProgress(75); + } + if (versionCode) { + const instance = await prisma.workflowInstances.findUnique({ + where: { + ProductId: job.data.productId + }, + select: { + Context: true + } + }); + const ctx: WorkflowInstanceContext = JSON.parse(instance.Context); + job.updateProgress(90); + // Use update here so this job doesn't inadvertently create a workflowInstance + await DatabaseWrites.workflowInstances.update(job.data.productId, { + Context: JSON.stringify({ + ...ctx, + environment: { + ...ctx.environment, + [ENVKeys.PUBLISH_GOOGLE_PLAY_UPLOADED_BUILD_ID]: '' + product.WorkflowBuildId, + [ENVKeys.PUBLISH_GOOGLE_PLAY_UPLOADED_VERSION_CODE]: '' + versionCode + } + } as WorkflowInstanceContext) + }); + } + job.updateProgress(100); + return versionCode; +} diff --git a/source/SIL.AppBuilder.Portal/node-server/job-executors/project.ts b/source/SIL.AppBuilder.Portal/node-server/job-executors/project.ts new file mode 100644 index 0000000000..2bea3c16c5 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/job-executors/project.ts @@ -0,0 +1,168 @@ +import { Job } from 'bullmq'; +import { BuildEngine, BullMQ, DatabaseWrites, prisma, Queues } from 'sil.appbuilder.portal.common'; + +export async function create(job: Job): Promise { + const projectData = await prisma.projects.findUnique({ + where: { + Id: job.data.projectId + }, + select: { + OrganizationId: true, + ApplicationType: { + select: { + Name: true + } + }, + Name: true, + Language: true + } + }); + job.updateProgress(25); + const response = await BuildEngine.Requests.createProject( + { type: 'query', organizationId: projectData.OrganizationId }, + { + app_id: projectData.ApplicationType.Name, + project_name: projectData.Name, + language_code: projectData.Language, + storage_type: 's3' + } + ); + job.updateProgress(50); + if (response.responseType === 'error') { + job.log(response.message); + throw new Error(`Creation of Project #${job.data.projectId} failed!`); + } else { + await DatabaseWrites.projects.update(job.data.projectId, { + WorkflowProjectId: response.id, + WorkflowAppProjectUrl: `${process.env.UI_URL ?? 'http://localhost:5173'}/projects/${ + job.data.projectId + }` + }); + job.updateProgress(75); + + await Queues.RemotePolling.add( + `Check status of Project #${response.id}`, + { + type: BullMQ.JobType.Project_Check, + workflowProjectId: response.id, + organizationId: projectData.OrganizationId, + projectId: job.data.projectId + }, + BullMQ.RepeatEveryMinute + ); + + job.updateProgress(100); + return response; + } +} + +export async function check(job: Job): Promise { + const response = await BuildEngine.Requests.getProject( + { type: 'query', organizationId: job.data.organizationId }, + job.data.workflowProjectId + ); + job.updateProgress(50); + if (response.responseType === 'error') { + job.log(response.message); + throw new Error(response.message); + } else { + if (response.status === 'completed') { + await Queues.RemotePolling.removeRepeatableByKey(job.repeatJobKey); + if (response.error) { + job.log(response.error); + } else { + await DatabaseWrites.projects.update(job.data.projectId, { + WorkflowProjectUrl: response.url + }); + + const projectImport = ( + await prisma.projects.findUnique({ + where: { + Id: job.data.projectId + }, + select: { + ProjectImport: { + select: { + Id: true + } + } + } + }) + )?.ProjectImport; + if (projectImport) { + await Queues.Miscellaneous.add(`Import Products for Project #${job.data.projectId}`, { + type: BullMQ.JobType.Project_ImportProducts, + organizationId: job.data.organizationId, + importId: projectImport.Id + }); + } + } + } + job.updateProgress(100); + return response; + } +} + +export async function importProducts(job: Job): Promise { + const projectImport = await prisma.projectImports.findUnique({ + where: { + Id: job.data.importId + } + }); + const project = await prisma.projects.findFirst({ + where: { + ImportId: job.data.importId + }, + select: { + Id: true + } + }); + job.updateProgress(25); + const productsToCreate: { Name: string; Store: string }[] = JSON.parse( + projectImport.ImportData + ).Products; + job.updateProgress(30); + const products = await Promise.all( + productsToCreate.map(async (p) => ({ + ...p, + Id: await DatabaseWrites.products.create({ + ProjectId: project.Id, + ProductDefinitionId: ( + await prisma.productDefinitions.findFirst({ + where: { + Name: p.Name, + OrganizationProductDefinitions: { + some: { + OrganizationId: job.data.organizationId + } + } + }, + select: { + Id: true + } + }) + )?.Id, + StoreId: ( + await prisma.stores.findFirst({ + where: { + Name: p.Store, + OrganizationStores: { + some: { + OrganizationId: job.data.organizationId + } + } + }, + select: { + Id: true + } + }) + )?.Id, + WorkflowJobId: 0, + WorkflowBuildId: 0, + WorkflowPublishId: 0 + }) + })) + ); + job.updateProgress(100); + return { products }; +} diff --git a/source/SIL.AppBuilder.Portal/node-server/job-executors/publish.ts b/source/SIL.AppBuilder.Portal/node-server/job-executors/publish.ts new file mode 100644 index 0000000000..8ed84f50cd --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/job-executors/publish.ts @@ -0,0 +1,247 @@ +import { Job } from 'bullmq'; +import { + BuildEngine, + BullMQ, + DatabaseWrites, + prisma, + Queues, + Workflow +} from 'sil.appbuilder.portal.common'; +import { WorkflowAction } from 'sil.appbuilder.portal.common/workflow'; +import { + addProductPropertiesToEnvironment, + getWorkflowParameters +} from './common.build-publish.js'; + +export async function product(job: Job): Promise { + const productData = await prisma.products.findUnique({ + where: { + Id: job.data.productId + }, + select: { + Project: { + select: { + OrganizationId: true + } + }, + WorkflowJobId: true, + WorkflowBuildId: true, + WorkflowInstance: { + select: { + Id: true + } + } + } + }); + job.updateProgress(10); + const productBuild = await prisma.productBuilds.findFirst({ + where: { + BuildId: productData.WorkflowBuildId + }, + select: { + Id: true + } + }); + if (!productData.WorkflowBuildId || !productBuild) { + const flow = await Workflow.restore(job.data.productId); + // TODO: Send notification of failure + flow?.send({ + type: WorkflowAction.Publish_Failed, + userId: null, + comment: 'Product does not have a ProductBuild available.' + }); + job.updateProgress(100); + return productData; + } + job.updateProgress(15); + if (productData.WorkflowInstance) { + await DatabaseWrites.products.update(job.data.productId, { + WorkflowPublishId: 0 + }); + job.updateProgress(20); + const params = await getWorkflowParameters(productData.WorkflowInstance.Id, 'publish'); + const channel = params['channel'] ?? job.data.defaultChannel; + job.updateProgress(30); + const env = await addProductPropertiesToEnvironment(job.data.productId); + job.updateProgress(40); + const response = await BuildEngine.Requests.createRelease( + { type: 'query', organizationId: productData.Project.OrganizationId }, + productData.WorkflowJobId, + productData.WorkflowBuildId, + { + channel: channel, + targets: params['targets'] ?? job.data.defaultTargets, + environment: { ...env, ...params.environment, ...job.data.environment } + } + ); + job.updateProgress(50); + if (response.responseType === 'error') { + const flow = await Workflow.restore(job.data.productId); + // TODO: Send notification of failure + flow?.send({ type: WorkflowAction.Publish_Failed, userId: null, comment: response.message }); + } else { + await DatabaseWrites.products.update(job.data.productId, { + WorkflowPublishId: response.id + }); + job.updateProgress(65); + + const pub = await DatabaseWrites.productPublications.create({ + data: { + ProductId: job.data.productId, + ProductBuildId: productBuild.Id, + ReleaseId: response.id, + Channel: channel + } + }); + + job.updateProgress(85); + + await Queues.RemotePolling.add( + `Check status of Publish #${response.id}`, + { + type: BullMQ.JobType.Publish_Check, + productId: job.data.productId, + organizationId: productData.Project.OrganizationId, + jobId: productData.WorkflowJobId, + buildId: productData.WorkflowBuildId, + releaseId: response.id, + publicationId: pub.Id + }, + BullMQ.RepeatEveryMinute + ); + } + job.updateProgress(100); + return { + response: { + ...response, + environment: + response.responseType !== 'error' + ? JSON.parse(response['environment'] ?? '{}') + : undefined + }, + params, + env + }; + } else { + job.log('No WorkflowInstance found. Workflow cancelled?'); + job.updateProgress(100); + return { productData }; + } +} + +export async function check(job: Job): Promise { + const product = await prisma.products.findFirst({ + where: { + WorkflowJobId: job.data.jobId, + WorkflowBuildId: job.data.buildId, + WorkflowPublishId: job.data.releaseId + }, + select: { + WorkflowInstance: { + select: { + Id: true + } + } + } + }); + if (!product?.WorkflowInstance) { + await Queues.RemotePolling.removeRepeatableByKey(job.repeatJobKey); + job.log('No WorkflowInstance found. Workflow cancelled?'); + job.updateProgress(100); + return { product }; + } + job.updateProgress(25); + const response = await BuildEngine.Requests.getRelease( + { type: 'query', organizationId: job.data.organizationId }, + job.data.jobId, + job.data.buildId, + job.data.releaseId + ); + job.updateProgress(50); + if (response.responseType === 'error') { + throw new Error(response.message); + } else { + if (response.status === 'completed') { + await Queues.RemotePolling.removeRepeatableByKey(job.repeatJobKey); + await Queues.Publishing.add( + `PostProcess Release #${job.data.releaseId} for Product #${job.data.productId}`, + { + type: BullMQ.JobType.Publish_PostProcess, + productId: job.data.productId, + publicationId: job.data.publicationId, + release: response + } + ); + } + job.updateProgress(100); + return { + ...response, + environment: JSON.parse(response['environment'] ?? '{}') + }; + } +} + +export async function postProcess(job: Job): Promise { + if (job.data.release.error) { + job.log(job.data.release.error); + } + let packageName: string | undefined = undefined; + const flow = await Workflow.restore(job.data.productId); + job.updateProgress(25); + if (flow) { + if (job.data.release.result === 'SUCCESS') { + const publishUrlFile = job.data.release.artifacts['publishUrl']; + await DatabaseWrites.products.update(job.data.productId, { + DatePublished: new Date(), + PublishLink: publishUrlFile + ? (await fetch(publishUrlFile).then((r) => r.text()))?.trim() ?? undefined + : undefined + }); + flow.send({ type: WorkflowAction.Publish_Completed, userId: null }); + const packageFile = await prisma.productPublications.findUnique({ + where: { + Id: job.data.publicationId + }, + select: { + ProductBuild: { + select: { + ProductArtifacts: { + where: { + ArtifactType: 'package_name' + }, + select: { + Url: true + }, + take: 1 + } + } + } + } + }); + if (packageFile?.ProductBuild.ProductArtifacts[0]) { + packageName = await fetch(packageFile.ProductBuild.ProductArtifacts[0].Url).then((r) => + r.text() + ); + } + } else { + flow.send({ + type: WorkflowAction.Publish_Failed, + userId: null, + comment: `system.publish-failed,${job.data.release.artifacts['consoleText'] ?? ''}` + }); + } + } + job.updateProgress(75); + const publication = await DatabaseWrites.productPublications.update({ + where: { + Id: job.data.publicationId + }, + data: { + Success: job.data.release.result === 'SUCCESS', + LogUrl: job.data.release.consoleText, + Package: packageName?.trim() + } + }); + job.updateProgress(100); + return { publication }; +} diff --git a/source/SIL.AppBuilder.Portal/node-server/job-executors/system.ts b/source/SIL.AppBuilder.Portal/node-server/job-executors/system.ts new file mode 100644 index 0000000000..8748c7741a --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/job-executors/system.ts @@ -0,0 +1,94 @@ +import { Job } from 'bullmq'; +import { BuildEngine, BullMQ, DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; + +export async function checkStatuses(job: Job): Promise { + const organizations = await prisma.organizations.findMany({ + where: { + OR: [ + { + UseDefaultBuildEngine: null + }, + { + UseDefaultBuildEngine: false + } + ] + }, + select: { + BuildEngineUrl: true, + BuildEngineApiAccessToken: true + } + }); + // Add defaults + if (process.env.DEFAULT_BUILDENGINE_URL && process.env.DEFAULT_BUILDENGINE_API_ACCESS_TOKEN) { + organizations.push({ + BuildEngineUrl: process.env.DEFAULT_BUILDENGINE_URL, + BuildEngineApiAccessToken: process.env.DEFAULT_BUILDENGINE_API_ACCESS_TOKEN + }); + } + job.updateProgress(10); + // remove statuses that do not correspond to organizations + const removed = await DatabaseWrites.systemStatuses.deleteMany({ + where: { + BuildEngineUrl: { + notIn: organizations.map((o) => o.BuildEngineUrl) + }, + BuildEngineApiAccessToken: { + notIn: organizations.map((o) => o.BuildEngineApiAccessToken) + } + } + }); + job.updateProgress(20); + const systems = await prisma.systemStatuses.findMany({ + select: { + BuildEngineUrl: true, + BuildEngineApiAccessToken: true + } + }); + // Filter out url/token pairs that already exist in the status table + const filteredOrgs = organizations.filter( + (o) => + !systems.find( + (s) => + s.BuildEngineUrl === o.BuildEngineUrl && + s.BuildEngineApiAccessToken === o.BuildEngineApiAccessToken + ) + ); + job.updateProgress(30); + await DatabaseWrites.systemStatuses.createMany({ + data: filteredOrgs.map((o) => ({ ...o, SystemAvailable: false })) + }); + job.updateProgress(50); + const statuses = await Promise.all( + ( + await prisma.systemStatuses.findMany() + ).map(async (s) => { + const res = await BuildEngine.Requests.systemCheck({ + type: 'provided', + url: s.BuildEngineUrl, + token: s.BuildEngineApiAccessToken + }); + await DatabaseWrites.systemStatuses.update({ + where: { + Id: s.Id + }, + data: { + SystemAvailable: res.status === 200 + } + }); + return { + url: s.BuildEngineUrl, + // return first 4 characters of token for differentiation purposes + partialToken: s.BuildEngineApiAccessToken.substring(0, 4), + status: res.status, + error: res.responseType === 'error' ? res : undefined + }; + }) + ); + job.updateProgress(100); + return { + removed: removed.count, + added: filteredOrgs.length, + total: statuses.length, + statuses + }; +} diff --git a/source/SIL.AppBuilder.Portal/node-server/job-executors/userTasks.ts b/source/SIL.AppBuilder.Portal/node-server/job-executors/userTasks.ts new file mode 100644 index 0000000000..dafa468b61 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/job-executors/userTasks.ts @@ -0,0 +1,187 @@ +import { Prisma } from '@prisma/client'; +import { Job } from 'bullmq'; +import { BullMQ, DatabaseWrites, prisma, Workflow } from 'sil.appbuilder.portal.common'; +import { RoleId } from 'sil.appbuilder.portal.common/prisma'; +import { ActionType } from 'sil.appbuilder.portal.common/workflow'; + +export async function modify(job: Job): Promise { + const products = await prisma.products.findMany({ + where: { + Id: job.data.scope === 'Product' ? job.data.productId : undefined, + ProjectId: job.data.scope === 'Project' ? job.data.projectId : undefined, + WorkflowInstance: + // WorkflowInstance can be null if deleting user tasks + job.data.operation.type !== BullMQ.UserTasks.OpType.Delete ? { isNot: null } : undefined + }, + select: { + Id: true, + ProjectId: true + } + }); + job.updateProgress(10); + const projectId = job.data.scope === 'Project' ? job.data.projectId : products[0].ProjectId; + + const productIds = products.map((p) => p.Id); + + let createdTasks: Prisma.UserTasksCreateManyInput[] = []; + let deletedCount = 0; + + // Clear PreExecuteEntries + await DatabaseWrites.productTransitions.deleteMany({ + where: { + WorkflowUserId: null, + ProductId: { in: productIds }, + DateTransition: null + } + }); + + job.updateProgress(20); + + let mapping: { + from: string; + to: string; + count: number; + }[] = []; + + if (job.data.operation.type === BullMQ.UserTasks.OpType.Reassign) { + const timestamp = new Date(); + + mapping = await Promise.all( + job.data.operation.userMapping.map(async (u) => ({ + from: ( + await prisma.users.findUnique({ where: { Id: u.from }, select: { Name: true } }) + ).Name, + to: (await prisma.users.findUnique({ where: { Id: u.to }, select: { Name: true } })).Name, + count: ( + await DatabaseWrites.userTasks.updateMany({ + where: { + UserId: u.from + }, + data: { + UserId: u.to, + DateUpdated: timestamp + } + }) + ).count + })) + ); + job.updateProgress(40); + for (let i = 0; i < products.length; i++) { + const snap = await Workflow.getSnapshot(products[i].Id); + job.updateProgress(40 + ((i + 0.2) * 40) / products.length); + await DatabaseWrites.productTransitions.createMany({ + data: await Workflow.transitionEntriesFromState(snap.state, products[i].Id, snap.config) + }); + job.updateProgress(40 + ((i + 1) * 40) / products.length); + } + job.updateProgress(80); + // Just in case the user had already existing tasks before the reassignment + createdTasks = await prisma.userTasks.findMany({ + where: { + UserId: { in: job.data.operation.userMapping.map((u) => u.to) }, + DateUpdated: { + gte: timestamp + } + } + }); + job.updateProgress(90); + } else { + job.updateProgress(25); + const allUsers = await DatabaseWrites.userRoles.allUsersByRole( + projectId, + job.data.operation.roles + ); + job.updateProgress(30); + if (job.data.operation.type !== BullMQ.UserTasks.OpType.Create) { + // Clear existing UserTasks + const res = await DatabaseWrites.userTasks.deleteMany({ + where: { + ProductId: { in: productIds }, + UserId: + !(job.data.operation.users || job.data.operation.roles) || + job.data.operation.type === BullMQ.UserTasks.OpType.Update + ? undefined + : { + in: + job.data.operation.users ?? + Array.from( + new Set(Object.entries(allUsers).map(([user, roles]) => parseInt(user))) + ) + }, + Role: job.data.operation.roles ? { in: job.data.operation.roles } : undefined + } + }); + deletedCount = res.count; + job.updateProgress(job.data.operation.type === BullMQ.UserTasks.OpType.Delete ? 90 : 40); + } + if (job.data.operation.type !== BullMQ.UserTasks.OpType.Delete) { + for (let i = 0; i < products.length; i++) { + const product = products[i]; + // Create tasks for all users that could perform this activity + const snap = await Workflow.getSnapshot(product.Id); + const roleSet = new Set( + ( + Workflow.availableTransitionsFromName(snap.state, snap.config) + .filter((t) => t[0].meta.type === ActionType.User) + .map((t) => t[0].meta.user) as RoleId[] + ).filter((r) => job.data.operation.roles?.includes(r) ?? true) + ); + job.updateProgress(40 + ((i + 0.33) * 40) / products.length); + createdTasks = Array.from( + new Set( + Object.entries(allUsers) + .filter(([users, roles]) => !roleSet.isDisjointFrom(roles)) + .map(([user, roles]) => parseInt(user)) + ) + ) + .filter((u) => job.data.operation.users?.includes(u) ?? true) + .flatMap((user) => + Array.from(roleSet).map((r) => ({ + UserId: user, + ProductId: product.Id, + ActivityName: snap.state, + Status: snap.state, + Comment: job.data.comment, + Role: r + })) + ); + await DatabaseWrites.userTasks.createMany({ + data: createdTasks + }); + job.updateProgress(40 + ((i + 0.67) * 40) / products.length); + await DatabaseWrites.productTransitions.createMany({ + data: await Workflow.transitionEntriesFromState(snap.state, products[i].Id, snap.config) + }); + job.updateProgress(40 + ((i + 1) * 40) / products.length); + } + job.updateProgress(80); + } + } + for (const task of createdTasks) { + // TODO: Send notification for the new task + // sendNotification(task); + } + job.updateProgress(100); + const userNameMap = await prisma.users.findMany({ + where: { + Id: { in: Array.from(new Set(createdTasks.map((t) => t.UserId))) } + }, + select: { + Id: true, + Name: true + } + }); + return { + deleted: deletedCount, + createdOrUpdated: { + count: createdTasks.length, + tasks: createdTasks.map((t) => ({ + productId: t.ProductId, + user: userNameMap.find((m) => m.Id === t.UserId).Name, + task: t.ActivityName, + roles: t.Role + })) + }, + reassignMap: mapping + }; +} diff --git a/source/SIL.AppBuilder.Portal/node-server/tsconfig.dev.json b/source/SIL.AppBuilder.Portal/node-server/tsconfig.dev.json new file mode 100644 index 0000000000..5dd2ecfe83 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/tsconfig.dev.json @@ -0,0 +1,9 @@ +{ + "exclude": ["index.ts"], + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "module": "NodeNext", + "moduleResolution": "NodeNext" + } +} diff --git a/source/SIL.AppBuilder.Portal/node-server/tsconfig.json b/source/SIL.AppBuilder.Portal/node-server/tsconfig.json new file mode 100644 index 0000000000..180eccc530 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "NodeNext", + "module": "NodeNext", + "outDir": "../out" + }, + "exclude": ["dev.ts"] +} diff --git a/source/SIL.AppBuilder.Portal/package-lock.json b/source/SIL.AppBuilder.Portal/package-lock.json new file mode 100644 index 0000000000..8952432eba --- /dev/null +++ b/source/SIL.AppBuilder.Portal/package-lock.json @@ -0,0 +1,9295 @@ +{ + "name": "sil.appbuilder.portal", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sil.appbuilder.portal", + "version": "0.0.1", + "dependencies": { + "@auth/express": "^0.8.4", + "@auth/sveltekit": "^1.7.4", + "@bull-board/express": "^6.7.1", + "express": "^4.21.2", + "sil.appbuilder.portal.common": "file:common" + }, + "devDependencies": { + "@auth/core": "^0.37.4", + "@iconify/svelte": "^4.2.0", + "@inlang/paraglide-js": "1.11.8", + "@inlang/paraglide-sveltekit": "^0.15.5", + "@playwright/test": "^1.50.0", + "@sveltejs/adapter-node": "^5.2.12", + "@sveltejs/kit": "^2.16.1", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@types/express": "^5.0.0", + "@types/node": "^22.10.10", + "@typescript-eslint/eslint-plugin": "^8.21.0", + "@typescript-eslint/parser": "^8.21.0", + "@vvo/tzdb": "^6.159.0", + "autoprefixer": "^10.4.20", + "commander": "^13.1.0", + "daisyui": "^4.12.23", + "eslint": "^9.19.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-svelte": "^2.46.1", + "fuse.js": "^7.0.0", + "postcss": "^8.5.1", + "prettier": "^3.4.2", + "prettier-plugin-prisma": "^5.0.0", + "prettier-plugin-svelte": "^3.3.3", + "svelte": "^5.19.2", + "svelte-check": "^4.1.4", + "svelte-flatpickr-plus": "^2.0.6", + "sveltekit-superforms": "^2.23.1", + "svelvet": "^10.0.2", + "tailwindcss": "^3.4.17", + "ts-node": "^10.9.2", + "tslib": "^2.8.1", + "typescript": "^5.7.3", + "valibot": "^1.0.0-beta.14", + "vite": "^6.0.11", + "vitest": "^3.0.4" + } + }, + "common": { + "name": "sil.appbuilder.portal.common", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^5.18.0", + "bullmq": "^5.12.2", + "xstate": "^5.18.2" + }, + "devDependencies": { + "prisma": "^5.18.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@ark/schema": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@ark/schema/-/schema-0.43.1.tgz", + "integrity": "sha512-B28ceusolthB9bmHQEG9Lp16keUTt5JhuoB9Uhaw7MQh9+ODSbH4d2KJDD/yrMSeoXiNqEGuWmhlR6YO2pdNvA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@ark/util": "0.43.1" + } + }, + "node_modules/@ark/util": { + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@ark/util/-/util-0.43.1.tgz", + "integrity": "sha512-sRx5bZiNoilA7mr5lAu78ZHOJx88nCagLER9Ns1FfoWWHrDWj8J8xU+VFj0g1ujJrAxWsVJRVElOEZ0XzXCrDw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@auth/core": { + "version": "0.37.4", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.4.tgz", + "integrity": "sha512-HOXJwXWXQRhbBDHlMU0K/6FT1v+wjtzdKhsNg0ZN7/gne6XPsIrjZ4daMcFnbq0Z/vsAbYBinQhhua0d77v7qw==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^5.9.6", + "oauth4webapi": "^3.1.1", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/express": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@auth/express/-/express-0.8.4.tgz", + "integrity": "sha512-6D38JtAI/ttGv8juc69r4Y7ozE2Un6Ayvu5+Sx2N41E+FPyISnWIvTrvO8MO7ltbHI8yErhQaE327jDsP9JglQ==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.37.4" + }, + "peerDependencies": { + "express": "^4.18.2" + } + }, + "node_modules/@auth/sveltekit": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@auth/sveltekit/-/sveltekit-1.8.0.tgz", + "integrity": "sha512-qCkN84BhDGxz99DFEIp6Y2rJffkKq6a/PYTNXvhfBqNKYWAMUbmhbV52LWKXX7QtpWzWaAhj2Nc3DZTdjgj3Bw==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.38.0", + "set-cookie-parser": "^2.7.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.3", + "@sveltejs/kit": "^1.0.0 || ^2.0.0", + "nodemailer": "^6.6.5", + "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/sveltekit/node_modules/@auth/core": { + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.38.0.tgz", + "integrity": "sha512-ClHl44x4cY3wfJmHLpW+XrYqED0fZIzbHmwbExltzroCjR5ts3DLTWzADRba8mJFYZ8JIEJDa+lXnGl0E9Bl7Q==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/sveltekit/node_modules/jose": { + "version": "6.0.8", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.8.tgz", + "integrity": "sha512-EyUPtOKyTYq+iMOszO42eobQllaIjJnwkZ2U93aJzNyPibCy7CEvT9UQnaCVB51IAd49gbNdCew1c0LcLTCB2g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", + "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bull-board/api": { + "version": "6.7.10", + "resolved": "https://registry.npmjs.org/@bull-board/api/-/api-6.7.10.tgz", + "integrity": "sha512-FY2k81Pei+fW7UK85mEjV6qGvY7uWn2sB4oNy2K+dsHZQ/sbpXMlcaJZEl+65sEmi9oFF/ush3DvmeSUCWbaCQ==", + "license": "MIT", + "dependencies": { + "redis-info": "^3.0.8" + }, + "peerDependencies": { + "@bull-board/ui": "6.7.10" + } + }, + "node_modules/@bull-board/express": { + "version": "6.7.10", + "resolved": "https://registry.npmjs.org/@bull-board/express/-/express-6.7.10.tgz", + "integrity": "sha512-spNL0TwwDYLkwKG6AbM+50+qhHdGQqAwUozPQNBEQO/+PxO4vBNZ6eSKHZ19xYGRxb9q0DQYywuSzft44Bt24A==", + "license": "MIT", + "dependencies": { + "@bull-board/api": "6.7.10", + "@bull-board/ui": "6.7.10", + "ejs": "^3.1.10", + "express": "^4.21.1 || ^5.0.0" + } + }, + "node_modules/@bull-board/ui": { + "version": "6.7.10", + "resolved": "https://registry.npmjs.org/@bull-board/ui/-/ui-6.7.10.tgz", + "integrity": "sha512-FRMCU7t1K0XKxFIiooBG8es0otK3fyW4BwCz5rfMwx7gpkHhYcp2qxI91vqazTWFXdScUpiWdDwCV7pJcRV+Gg==", + "license": "MIT", + "dependencies": { + "@bull-board/api": "6.7.10" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", + "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.12.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@exodus/schemasafe": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@gcornut/valibot-json-schema": { + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@gcornut/valibot-json-schema/-/valibot-json-schema-0.31.0.tgz", + "integrity": "sha512-3xGptCurm23e7nuPQkdrE5rEs1FeTPHhAUsBuwwqG4/YeZLwJOoYZv+fmsppUEfo5y9lzUwNQrNqLS/q7HMc7g==", + "dev": true, + "optional": true, + "dependencies": { + "valibot": "~0.31.0" + }, + "bin": { + "valibot-json-schema": "bin/index.js" + }, + "optionalDependencies": { + "@types/json-schema": ">= 7.0.14", + "esbuild": ">= 0.18.20", + "esbuild-runner": ">= 2.2.2" + } + }, + "node_modules/@gcornut/valibot-json-schema/node_modules/valibot": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.31.1.tgz", + "integrity": "sha512-2YYIhPrnVSz/gfT2/iXVTrSj92HwchCt9Cga/6hX4B26iCz9zkIsGTS0HjDYTZfTi1Un0X6aRvhBi1cfqs/i0Q==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@iconify/svelte": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@iconify/svelte/-/svelte-4.2.0.tgz", + "integrity": "sha512-fEl0T7SAPonK7xk6xUlRPDmFDZVDe2Z7ZstlqeDS/sS8ve2uyU+Qa8rTWbIqzZJlRvONkK5kVXiUf9nIc+6OOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/types": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/cyberalien" + }, + "peerDependencies": { + "svelte": ">4.0.0" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inlang/detect-json-formatting": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@inlang/detect-json-formatting/-/detect-json-formatting-1.0.0.tgz", + "integrity": "sha512-o0jeI8U4TgNlsPwI0y92jld8/18Loh2KEgHCYCJ42rCOdxFrA8R60cydlEd2/6jkdHFn5DxKj8rOyiKv3z9uOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "guess-json-indent": "2.0.0" + } + }, + "node_modules/@inlang/json-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inlang/json-types/-/json-types-1.1.0.tgz", + "integrity": "sha512-n6vS6AqETsCFbV4TdBvR/EH57waVXzKsMqeUQ+eH2Q6NUATfKhfLabgNms2A+QV3aedH/hLtb1pRmjl2ykBVZg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "@sinclair/typebox": "^0.31.0" + } + }, + "node_modules/@inlang/language-tag": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@inlang/language-tag/-/language-tag-1.5.1.tgz", + "integrity": "sha512-+NlYDxDvN5h/TKUmkuQv+Ct1flxaVRousCbek7oFEk3/afZPVLNTJhm+cX2xiOg3tmi2KKrBLfy/V9oUDHj6GQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sinclair/typebox": "^0.31.17" + } + }, + "node_modules/@inlang/message": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@inlang/message/-/message-2.1.0.tgz", + "integrity": "sha512-Gr3wiErI7fW4iW11xgZzsJEUTjlZuz02fB/EO+ENTBlSHGyI1kzbCCeNqLr1mnGdQYiOxfuZxY0S4G5C6Pju3Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inlang/language-tag": "1.5.1" + }, + "peerDependencies": { + "@sinclair/typebox": "^0.31.17" + } + }, + "node_modules/@inlang/message-lint-rule": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@inlang/message-lint-rule/-/message-lint-rule-1.4.7.tgz", + "integrity": "sha512-FCiFe/H25fqhsIb/YTb0K7eDJqEYzdr6ectF0xG4zARiS7nXz0FHxk2niJrIO8kFkB4mx6tszsgQ0xqD5cHQag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inlang/json-types": "1.1.0", + "@inlang/language-tag": "1.5.1", + "@inlang/message": "2.1.0", + "@inlang/project-settings": "2.4.2", + "@inlang/translatable": "1.3.1" + }, + "peerDependencies": { + "@sinclair/typebox": "^0.31.17" + } + }, + "node_modules/@inlang/module": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@inlang/module/-/module-1.2.14.tgz", + "integrity": "sha512-Z7rRa6x3RkzjdvNA7x+KskNGdSBEO46X9c7bTl6eZmLXy0J9yGDn6s4jpYqQzyKRG8g5mEqWcRqcVqdNwzj5Gg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inlang/message-lint-rule": "1.4.7", + "@inlang/plugin": "2.4.14" + }, + "peerDependencies": { + "@sinclair/typebox": "^0.31.17" + } + }, + "node_modules/@inlang/paraglide-js": { + "version": "1.11.8", + "resolved": "https://registry.npmjs.org/@inlang/paraglide-js/-/paraglide-js-1.11.8.tgz", + "integrity": "sha512-PxzrmDP63fbMNF4/AtiLFTnUodFxVbOkLpIrOzPZvNuLg0wCWnsaBfNT87/rNjL/A7ZPzEBmuDi0P2pn8iB0Fw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inlang/detect-json-formatting": "1.0.0", + "@inlang/language-tag": "1.5.1", + "@inlang/plugin-message-format": "2.2.0", + "@inlang/recommend-ninja": "0.1.1", + "@inlang/recommend-sherlock": "0.1.1", + "@inlang/sdk": "0.37.0", + "@lix-js/client": "2.2.1", + "@lix-js/fs": "2.2.0", + "commander": "11.1.0", + "consola": "3.2.3", + "dedent": "1.5.1", + "json5": "2.2.3", + "posthog-node": "^4.0.1" + }, + "bin": { + "paraglide-js": "bin/run.js" + } + }, + "node_modules/@inlang/paraglide-js/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@inlang/paraglide-sveltekit": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/@inlang/paraglide-sveltekit/-/paraglide-sveltekit-0.15.5.tgz", + "integrity": "sha512-6xLbZJAYbJBiXXriy+cCq4+SJ1u8vCIrom7m7akxNuG1G6F0kko3OTyMrouDca9WYR1+XD+uynn3wa5+LWTLTg==", + "dev": true, + "dependencies": { + "@inlang/paraglide-js": "1.11.8", + "@inlang/paraglide-vite": "1.3.5", + "@lix-js/client": "2.2.1", + "commander": "^12.0.0", + "dedent": "1.5.1", + "devalue": "^4.3.2", + "magic-string": "^0.30.5", + "svelte": "^5.0.0 || ^5.0.0-next.1 || ^5.0.0-rc.1" + }, + "bin": { + "paraglide-sveltekit": "bin/run.js" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.3" + } + }, + "node_modules/@inlang/paraglide-sveltekit/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inlang/paraglide-unplugin": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@inlang/paraglide-unplugin/-/paraglide-unplugin-1.9.5.tgz", + "integrity": "sha512-5KklLBvl/y+R4SccWH74USTGQNFW5IwEyMLQ3WIHX9cHX2pnnA5wGqQxYg3EcgCyErHLc3+sm7EMNB5Z0dSeTg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inlang/paraglide-js": "1.11.8", + "@inlang/sdk": "0.37.0", + "@lix-js/client": "2.2.1", + "typescript": "^5.5.2", + "unplugin": "^1.14.1" + } + }, + "node_modules/@inlang/paraglide-vite": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@inlang/paraglide-vite/-/paraglide-vite-1.3.5.tgz", + "integrity": "sha512-yLa+gxA8el6RXXneeiqTnV9Od4Yh389lA+wSfiS+jDXY5vV/2j7Lpk2yuATLmxwI9i2nMP6c6yu8L0X77PA9dg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inlang/paraglide-unplugin": "1.9.5" + } + }, + "node_modules/@inlang/plugin": { + "version": "2.4.14", + "resolved": "https://registry.npmjs.org/@inlang/plugin/-/plugin-2.4.14.tgz", + "integrity": "sha512-HFI1t1tKs6jXqwKVl59vvt7kvMgg2Po7xA3IFijfJTZCt0tTI8txqeXCUV9jhUop29Hqj6a5zQd32BYv33Dulw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inlang/json-types": "1.1.0", + "@inlang/language-tag": "1.5.1", + "@inlang/message": "2.1.0", + "@inlang/project-settings": "2.4.2", + "@inlang/translatable": "1.3.1", + "@lix-js/fs": "2.2.0" + }, + "peerDependencies": { + "@sinclair/typebox": "^0.31.17" + } + }, + "node_modules/@inlang/plugin-message-format": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inlang/plugin-message-format/-/plugin-message-format-2.2.0.tgz", + "integrity": "sha512-6MJLExr3OLqbR8gCP4UEgNMgdaJFFCug2GLmFwid7Ana4kObnbCA33YN3m3eN8p+lmnv7zpfW7oeyTZXZLoptg==", + "dev": true + }, + "node_modules/@inlang/project-settings": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@inlang/project-settings/-/project-settings-2.4.2.tgz", + "integrity": "sha512-Okus2JdwTzNebZHkXCrUH/zIWwqu7kWm/ZQaM6a31oRIEA2JdQJtyNGM8E/KrwGfEuq18U+WV03+tR3tkwsGvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inlang/json-types": "1.1.0", + "@inlang/language-tag": "1.5.1" + }, + "peerDependencies": { + "@sinclair/typebox": "^0.31.17" + } + }, + "node_modules/@inlang/recommend-ninja": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@inlang/recommend-ninja/-/recommend-ninja-0.1.1.tgz", + "integrity": "sha512-dthW8SA6LHUhPFXwKxYy92PG4dg4KeIS0jbgpplXxgoQAeouP6DHEa87kva2DXbk3kUbNz+/MFPjyaygBfamog==", + "dev": true, + "dependencies": { + "@inlang/sdk": "0.36.3", + "@lix-js/client": "2.2.1", + "@lix-js/fs": "2.2.0", + "@sinclair/typebox": "^0.31.17", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@inlang/recommend-ninja/node_modules/@inlang/sdk": { + "version": "0.36.3", + "resolved": "https://registry.npmjs.org/@inlang/sdk/-/sdk-0.36.3.tgz", + "integrity": "sha512-wjsavc44H24v74tdEQ13FqZZcr43T106oEfHJnBLzEP55Zz2JJWABLund+DEdosZx+9E8mJBEW5JlVnlBwP3Zw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inlang/json-types": "1.1.0", + "@inlang/language-tag": "1.5.1", + "@inlang/message": "2.1.0", + "@inlang/message-lint-rule": "1.4.7", + "@inlang/module": "1.2.14", + "@inlang/plugin": "2.4.14", + "@inlang/project-settings": "2.4.2", + "@inlang/result": "1.1.0", + "@inlang/translatable": "1.3.1", + "@lix-js/client": "2.2.1", + "@lix-js/fs": "2.2.0", + "@sinclair/typebox": "^0.31.17", + "debug": "^4.3.4", + "dedent": "1.5.1", + "deepmerge-ts": "^5.1.0", + "murmurhash3js": "^3.0.1", + "solid-js": "1.6.12", + "throttle-debounce": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@inlang/recommend-sherlock": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@inlang/recommend-sherlock/-/recommend-sherlock-0.1.1.tgz", + "integrity": "sha512-8qZ8FJ/QqVh6YqKmHo3SxI4ENM0O80TCzETm+hxeQ2JzPKPFYucFINpLvUygiLFp/hJwhoI5TjRz6jNI2QdfMQ==", + "dev": true, + "dependencies": { + "@inlang/sdk": "0.36.4", + "@lix-js/fs": "2.2.0", + "@sinclair/typebox": "^0.31.17", + "comment-json": "^4.2.3" + } + }, + "node_modules/@inlang/recommend-sherlock/node_modules/@inlang/sdk": { + "version": "0.36.4", + "resolved": "https://registry.npmjs.org/@inlang/sdk/-/sdk-0.36.4.tgz", + "integrity": "sha512-fTr0mkDx2ViZt/8lxaF9Mxj3m8LaqIhcjMJy+CdHREMc9UvpUhGLB7elMp061YysxnN1CFccAgLRug5VWK3yWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inlang/json-types": "1.1.0", + "@inlang/language-tag": "1.5.1", + "@inlang/message": "2.1.0", + "@inlang/message-lint-rule": "1.4.7", + "@inlang/module": "1.2.14", + "@inlang/plugin": "2.4.14", + "@inlang/project-settings": "2.4.2", + "@inlang/result": "1.1.0", + "@inlang/translatable": "1.3.1", + "@lix-js/client": "2.2.1", + "@lix-js/fs": "2.2.0", + "@sinclair/typebox": "^0.31.17", + "debug": "^4.3.4", + "dedent": "1.5.1", + "deepmerge-ts": "^5.1.0", + "murmurhash3js": "^3.0.1", + "solid-js": "1.6.12", + "throttle-debounce": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@inlang/result": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inlang/result/-/result-1.1.0.tgz", + "integrity": "sha512-zLGroi9EUiHuOjUOaglUVTFO7EWdo2OARMJLBO1Q5Ga/xJmSQb6XS1lhqEXBFAjgFarfEMX5YEJWWALogYV3wA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@inlang/sdk": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/@inlang/sdk/-/sdk-0.37.0.tgz", + "integrity": "sha512-/uG/9HrJU+v5jY/nWKZAlI3diD8WdT5bAYuIZ3rVsnphvqV4iWvQwAp3H/K8F5QDJ+GEY79mhKfFhHcKMSiWng==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inlang/json-types": "1.1.0", + "@inlang/language-tag": "1.5.1", + "@inlang/message": "2.1.0", + "@inlang/message-lint-rule": "1.4.7", + "@inlang/module": "1.2.14", + "@inlang/plugin": "2.4.14", + "@inlang/project-settings": "2.4.2", + "@inlang/result": "1.1.0", + "@inlang/translatable": "1.3.1", + "@lix-js/client": "2.2.1", + "@lix-js/fs": "2.2.0", + "@sinclair/typebox": "^0.31.17", + "debug": "^4.3.4", + "dedent": "1.5.1", + "deepmerge-ts": "^5.1.0", + "murmurhash3js": "^3.0.1", + "solid-js": "1.6.12", + "throttle-debounce": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@inlang/translatable": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@inlang/translatable/-/translatable-1.3.1.tgz", + "integrity": "sha512-VAtle21vRpIrB+axtHFrFB0d1HtDaaNj+lV77eZQTJyOWbTFYTVIQJ8WAbyw9eu4F6h6QC2FutLyxjMomxfpcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inlang/language-tag": "1.5.1" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lix-js/client": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@lix-js/client/-/client-2.2.1.tgz", + "integrity": "sha512-6DTJdRN2L2a1A8OxW1Wqh3ZOORqq8+YlCALMF5UMoxhfHE4Fcq9FZztMkAV+KwhrDSsp0USWvD9myG0XX+v6QQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@lix-js/fs": "2.2.0", + "async-lock": "1.4.1", + "clean-git-ref": "2.0.1", + "crc-32": "1.2.2", + "ignore": "5.3.1", + "octokit": "3.1.2", + "pako": "1.0.11", + "pify": "5.0.0", + "sha.js": "2.4.11" + } + }, + "node_modules/@lix-js/fs": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@lix-js/fs/-/fs-2.2.0.tgz", + "integrity": "sha512-B9X3FjD8WmdG7tbA44JuniSO0KdKBWnjfxl8zpgrDCkavrp/GP7U0xxBkc0WgeeoHjQ/pkqq9VqtWB2kS9jIUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "typescript": "5.2.2" + } + }, + "node_modules/@lix-js/fs/node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@octokit/app": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@octokit/app/-/app-14.1.0.tgz", + "integrity": "sha512-g3uEsGOQCBl1+W1rgfwoRFUIR6PtvB2T1E4RpygeUU5LrLvlOqcxrt5lfykIeRpUPpupreGJUYl70fqMDXdTpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-app": "^6.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/types": "^12.0.0", + "@octokit/webhooks": "^12.0.4" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-6.1.3.tgz", + "integrity": "sha512-dcaiteA6Y/beAlDLZOPNReN3FGHu+pARD6OHfh3T9f3EO09++ec+5wt3KtGGSSs2Mp5tI8fQwdMOEnrzBLfgUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^7.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "deprecation": "^2.3.1", + "lru-cache": "npm:@wolfy1339/lru-cache@^11.0.2-patch.1", + "universal-github-app-jwt": "^1.1.2", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", + "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-app/node_modules/@octokit/types": { + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", + "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^23.0.1" + } + }, + "node_modules/@octokit/auth-oauth-app": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-7.1.0.tgz", + "integrity": "sha512-w+SyJN/b0l/HEb4EOPRudo7uUOSW51jcK1jwLa+4r7PA8FPFpoxEnHBHMITqCsc/3Vo2qqFjgQfz/xUUvsSQnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/auth-oauth-user": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "@types/btoa-lite": "^1.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", + "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": { + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", + "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^23.0.1" + } + }, + "node_modules/@octokit/auth-oauth-device": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-6.1.0.tgz", + "integrity": "sha512-FNQ7cb8kASufd6Ej4gnJ3f1QB5vJitkoV1O0/g6e6lUsQ7+VsSNRHRmFScN2tV4IgKA12frrr/cegUs0t+0/Lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", + "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": { + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", + "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^23.0.1" + } + }, + "node_modules/@octokit/auth-oauth-user": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-4.1.0.tgz", + "integrity": "sha512-FrEp8mtFuS/BrJyjpur+4GARteUCrPeR/tZJzD8YourzoVhRics7u7we/aDcKv+yywRNwNi/P4fRi631rG/OyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-device": "^6.1.0", + "@octokit/oauth-methods": "^4.1.0", + "@octokit/request": "^8.3.1", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", + "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": { + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", + "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^23.0.1" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/auth-unauthenticated": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-5.0.1.tgz", + "integrity": "sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", + "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core/node_modules/@octokit/openapi-types": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", + "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/core/node_modules/@octokit/types": { + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", + "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^23.0.1" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", + "integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", + "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/endpoint/node_modules/@octokit/types": { + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", + "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^23.0.1" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz", + "integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.4.1", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", + "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/graphql/node_modules/@octokit/types": { + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", + "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^23.0.1" + } + }, + "node_modules/@octokit/oauth-app": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-6.1.0.tgz", + "integrity": "sha512-nIn/8eUJ/BKUVzxUXd5vpzl1rwaVxMyYbQkNZjHrF7Vk/yu98/YDF/N2KeWO7uZ0g3b5EyiFXFkZI8rJ+DH1/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-oauth-app": "^7.0.0", + "@octokit/auth-oauth-user": "^4.0.0", + "@octokit/auth-unauthenticated": "^5.0.0", + "@octokit/core": "^5.0.0", + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/oauth-methods": "^4.0.0", + "@types/aws-lambda": "^8.10.83", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-authorization-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-6.0.2.tgz", + "integrity": "sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-4.1.0.tgz", + "integrity": "sha512-4tuKnCRecJ6CG6gr0XcEXdZtkTDbfbnD5oaHBmLERTjTMZNi2CbfEHZxPU41xXLDG4DfKf+sonu00zvKI9NSbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/oauth-authorization-url": "^6.0.2", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "btoa-lite": "^1.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", + "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/oauth-methods/node_modules/@octokit/types": { + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", + "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^23.0.1" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-graphql": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-4.0.1.tgz", + "integrity": "sha512-R8ZQNmrIKKpHWC6V2gum4x9LG2qF1RxRjo27gjQcG3j+vf2tLsEfE7I/wRWEPzYMaenr1M+qDAtNcwZve1ce1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.2.tgz", + "integrity": "sha512-u3KYkGF7GcZnSD/3UP0S7K5XUFT2FkOQdcfXZGZQPGv3lm4F2Xbf71lvjldr8c1H3nNbF+33cLEkWYbokGWqiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz", + "integrity": "sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.1.0.tgz", + "integrity": "sha512-WrO3bvq4E1Xh1r2mT9w6SDFg01gFmP81nIG77+p/MqW1JeXXgL++6umim3t6x0Zj5pZm3rXAN+0HEjmmdhIRig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^13.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", + "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", + "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^23.0.1" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz", + "integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.2.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz", + "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.6", + "@octokit/request-error": "^5.1.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz", + "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", + "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/request-error/node_modules/@octokit/types": { + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", + "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^23.0.1" + } + }, + "node_modules/@octokit/request/node_modules/@octokit/openapi-types": { + "version": "23.0.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-23.0.1.tgz", + "integrity": "sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/request/node_modules/@octokit/types": { + "version": "13.8.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.8.0.tgz", + "integrity": "sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^23.0.1" + } + }, + "node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@octokit/webhooks": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks/-/webhooks-12.3.1.tgz", + "integrity": "sha512-BVwtWE3rRXB9IugmQTfKspqjNa8q+ab73ddkV9k1Zok3XbuOxJUi4lTYk5zBZDhfWb/Y2H+RO9Iggm25gsqeow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/webhooks-methods": "^4.1.0", + "@octokit/webhooks-types": "7.6.1", + "aggregate-error": "^3.1.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks-methods": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-4.1.0.tgz", + "integrity": "sha512-zoQyKw8h9STNPqtm28UGOYFE7O6D4Il8VJwhAtMHFt2C4L0VQT1qGKLeefUOqHNs1mNRYSadVv7x0z8U2yyeWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/webhooks-types": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@octokit/webhooks-types/-/webhooks-types-7.6.1.tgz", + "integrity": "sha512-S8u2cJzklBC0FgTwWVLaM8tMrDuDMVE4xiTK4EYXM9GntyvrdbSoxqDQa+Fh57CCNApyIpyeqPhhFEmHPfrXgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@playwright/test": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz", + "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.50.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "license": "MIT" + }, + "node_modules/@poppinss/macroable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@poppinss/macroable/-/macroable-1.0.4.tgz", + "integrity": "sha512-ct43jurbe7lsUX5eIrj4ijO3j/6zIPp7CDnFWXDs7UPAbw1Pu1iH3oAmFdP4jcskKJBURH5M9oTtyeiUXyHX8Q==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.16.0" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "node_modules/@prisma/prisma-schema-wasm": { + "version": "4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584", + "resolved": "https://registry.npmjs.org/@prisma/prisma-schema-wasm/-/prisma-schema-wasm-4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584.tgz", + "integrity": "sha512-JFdsnSgBPN8reDTLOI9Vh/6ccCb2aD1LbY/LWQnkcIgNo6IdpzvuM+qRVbBuA6IZP2SdqQI8Lu6RL2P8EFBQUA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "28.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.2.tgz", + "integrity": "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "fdir": "^6.2.0", + "is-reference": "1.2.1", + "magic-string": "^0.30.3", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=16.0.0 || 14 >= 14.17" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.0.tgz", + "integrity": "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz", + "integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz", + "integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", + "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", + "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz", + "integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz", + "integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz", + "integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz", + "integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", + "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", + "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz", + "integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz", + "integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz", + "integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz", + "integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", + "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", + "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", + "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz", + "integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", + "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/@sinclair/typebox": { + "version": "0.31.28", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.31.28.tgz", + "integrity": "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", + "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-node": { + "version": "5.2.12", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz", + "integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.0", + "rollup": "^4.9.5" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.4.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.18.0.tgz", + "integrity": "sha512-4DGCGiwNzgnPJySlMe/Qi6rKMK3ntphJaV95BTW+aggaTIAVZ5x3Bp+LURVLMxAEAtWAI5U449NafVxTS+kXbQ==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^0.6.0", + "devalue": "^5.1.0", + "esm-env": "^1.2.2", + "import-meta-resolve": "^4.1.0", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.3 || ^6.0.0" + } + }, + "node_modules/@sveltejs/kit/node_modules/devalue": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "license": "MIT" + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz", + "integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==", + "license": "MIT", + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", + "debug": "^4.4.0", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.15", + "vitefu": "^1.0.4" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", + "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.7" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "vite": "^6.0.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/aws-lambda": { + "version": "8.10.147", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.147.tgz", + "integrity": "sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/btoa-lite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/btoa-lite/-/btoa-lite-1.0.2.tgz", + "integrity": "sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", + "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.13.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz", + "integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/validator": { + "version": "13.12.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.2.tgz", + "integrity": "sha512-6SlHBzUW8Jhf3liqrGGXyTJSIFe4nqlJ5A5KaMZ2l/vbM3Wh3KSybots/wfWVzNLK4D1NZluDlSQIbIEPx6oyA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@typeschema/class-validator": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@typeschema/class-validator/-/class-validator-0.3.0.tgz", + "integrity": "sha512-OJSFeZDIQ8EK1HTljKLT5CItM2wsbgczLN8tMEfz3I1Lmhc5TBfkZ0eikFzUC16tI3d1Nag7um6TfCgp2I2Bww==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@typeschema/core": "0.14.0" + }, + "peerDependencies": { + "class-validator": "^0.14.1" + }, + "peerDependenciesMeta": { + "class-validator": { + "optional": true + } + } + }, + "node_modules/@typeschema/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@typeschema/core/-/core-0.14.0.tgz", + "integrity": "sha512-Ia6PtZHcL3KqsAWXjMi5xIyZ7XMH4aSnOQes8mfMLx+wGFGtGRNlwe6Y7cYvX+WfNK67OL0/HSe9t8QDygV0/w==", + "dev": true, + "license": "MIT", + "optional": true, + "peerDependencies": { + "@types/json-schema": "^7.0.15" + }, + "peerDependenciesMeta": { + "@types/json-schema": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz", + "integrity": "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.26.0", + "@typescript-eslint/type-utils": "8.26.0", + "@typescript-eslint/utils": "8.26.0", + "@typescript-eslint/visitor-keys": "8.26.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.0.tgz", + "integrity": "sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.26.0", + "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/typescript-estree": "8.26.0", + "@typescript-eslint/visitor-keys": "8.26.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz", + "integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/visitor-keys": "8.26.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz", + "integrity": "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.26.0", + "@typescript-eslint/utils": "8.26.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz", + "integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz", + "integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/visitor-keys": "8.26.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.0.tgz", + "integrity": "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.26.0", + "@typescript-eslint/types": "8.26.0", + "@typescript-eslint/typescript-estree": "8.26.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz", + "integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.26.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vinejs/compiler": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vinejs/compiler/-/compiler-3.0.0.tgz", + "integrity": "sha512-v9Lsv59nR56+bmy2p0+czjZxsLHwaibJ+SV5iK9JJfehlJMa501jUJQqqz4X/OqKXrxtE3uTQmSqjUqzF3B2mw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@vinejs/vine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@vinejs/vine/-/vine-3.0.0.tgz", + "integrity": "sha512-GeCAHLzKkL2kMFqatgqyiiNh+FILOSAV8x8imBDo6AWQ91w30Kaxw4FnzUDqgcd9z8aCYOBQ7RJxBBGfyr+USQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@poppinss/macroable": "^1.0.3", + "@types/validator": "^13.12.2", + "@vinejs/compiler": "^3.0.0", + "camelcase": "^8.0.0", + "dayjs": "^1.11.13", + "dlv": "^1.1.3", + "normalize-url": "^8.0.1", + "validator": "^13.12.0" + }, + "engines": { + "node": ">=18.16.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz", + "integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz", + "integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.0.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz", + "integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz", + "integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.0.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz", + "integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz", + "integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz", + "integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.0.7", + "loupe": "^3.1.3", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vvo/tzdb": { + "version": "6.161.0", + "resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.161.0.tgz", + "integrity": "sha512-rvk2x77vnY87Tu1d8QuJk300WWzk8OP9/cDw2KgxEdjlYpLarJx82j2sPUpiy1wnjSuTTnYwpjBgZnNS5Iyb+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arktype": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/arktype/-/arktype-2.1.2.tgz", + "integrity": "sha512-+uwghWzB1C0WoN6TpVB6wqXpshbRu19LykgddVcD8fk2SZHSgXWcxaTnq+cHnAqtZZOfJ/OI/QDTBmf9SiUFCw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@ark/schema": "0.43.1", + "@ark/util": "0.43.1" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/btoa-lite": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz", + "integrity": "sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/bullmq": { + "version": "5.41.7", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.7.tgz", + "integrity": "sha512-eZbKJSx15bflfzKRiR+dKeLTr/M/YKb4cIp73OdU79PEMHQ6aEFUtbG6R+f0KvLLznI/O01G581U2Eqli6S2ew==", + "license": "MIT", + "dependencies": { + "cron-parser": "^4.9.0", + "ioredis": "^5.4.1", + "msgpackr": "^1.11.2", + "node-abort-controller": "^3.1.1", + "semver": "^7.5.4", + "tslib": "^2.0.0", + "uuid": "^9.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz", + "integrity": "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001702", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001702.tgz", + "integrity": "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/class-validator": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.1.tgz", + "integrity": "sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.10.53", + "validator": "^13.9.0" + } + }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/comment-json": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.5.tgz", + "integrity": "sha512-bKw/r35jR3HGt5PEPm1ljsQQGyCrR8sFGNiN5L+ykDHdpO8Smxkrkla9Yi6NkQyUrb8V54PGhfMs6NrIwtxtdw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cron-parser": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", + "integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==", + "license": "MIT", + "dependencies": { + "luxon": "^3.2.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/culori": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.12.24", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.24.tgz", + "integrity": "sha512-JYg9fhQHOfXyLadrBrEqCDM6D5dWCSSiM6eTNCRrBRzx/VlOCrLS8eDfIw9RVvs64v2mJdLooKXY8EwQzoszAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deepmerge-ts": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-5.1.0.tgz", + "integrity": "sha512-eS8dRJOckyo9maw9Tu5O5RUi/4inFLrnoLkBe3cPfDMx3WZioXtmOew4TXQaxq7Rhl4xjDtR7c6x8nNTxOvbFw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-4.3.3.tgz", + "integrity": "sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg==", + "dev": true, + "license": "MIT" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.13.6", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.13.6.tgz", + "integrity": "sha512-NKmzyIuOb2UuHFPRz9EYScbhMBxXkzjPRuu+4axE+hMk1f0U7TZxzi2CP3TVVxA2kzvh00aBQEbyH7Opq4PnWg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.112", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.112.tgz", + "integrity": "sha512-oen93kVyqSb3l+ziUgzIOlWt/oOuy4zRmpwestMn4rhFWAoFJeFuCVte9F2fASjeZZo7l/Cif9TiyrdW4CwEMA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/esbuild-runner": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/esbuild-runner/-/esbuild-runner-2.2.2.tgz", + "integrity": "sha512-fRFVXcmYVmSmtYm2mL8RlUASt2TDkGh3uRcvHFOKNr/T58VrfVeKD9uT9nlgxk96u0LS0ehS/GY7Da/bXWKkhw==", + "dev": true, + "license": "Apache License 2.0", + "optional": true, + "dependencies": { + "source-map-support": "0.5.21", + "tslib": "2.4.0" + }, + "bin": { + "esr": "bin/esr.js" + }, + "peerDependencies": { + "esbuild": "*" + } + }, + "node_modules/esbuild-runner/node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", + "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.2", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.21.0", + "@eslint/plugin-kit": "^0.2.7", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.2.tgz", + "integrity": "sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "build/bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-svelte": { + "version": "2.46.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", + "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@jridgewell/sourcemap-codec": "^1.4.15", + "eslint-compat-utils": "^0.5.1", + "esutils": "^2.0.3", + "known-css-properties": "^0.35.0", + "postcss": "^8.4.38", + "postcss-load-config": "^3.1.4", + "postcss-safe-parser": "^6.0.0", + "postcss-selector-parser": "^6.1.0", + "semver": "^7.6.2", + "svelte-eslint-parser": "^0.43.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "license": "MIT" + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrap": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.5.tgz", + "integrity": "sha512-CjNMjkBWWZeHn+VX+gS8YvFwJ5+NDhg8aWZBSFJPR8qQduDNjbJodA2WcwCm7uQa5Rjqj+nZvVmceg1RbHFB9g==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expect-type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.0.tgz", + "integrity": "sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", + "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatpickr_plus": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/flatpickr_plus/-/flatpickr_plus-1.1.20.tgz", + "integrity": "sha512-yx0Dj9Wwv/yqaoy59+027vUNMc75MgFvznTAZUQZdHxTkntjns/HcFk0tP8fneQwxua426IIN9JU4ArLFMLIHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/guess-json-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/guess-json-indent/-/guess-json-indent-2.0.0.tgz", + "integrity": "sha512-3Tm6R43KhtZWEVSHZnFmYMV9+gf3Vu0HXNNYtPVk2s7o8eGwYlJPHrjLtYw/7HBc10YxV+bfzKMuOf24z5qFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ioredis": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.5.0.tgz", + "integrity": "sha512-7CutT89g23FfSa8MDoIFs2GYYa0PaNiW/OrT+nRyjRXHDZd17HmIgy+reOQ/yhh72NznNjGuS8kbCAcA4Ro4mw==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "devOptional": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/known-css-properties": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", + "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/libphonenumber-js": { + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.4.tgz", + "integrity": "sha512-vLmhg7Gan7idyAKfc6pvCtNzvar4/eIzrVVk3hjNFH5+fGqyjD0gQRovdTrDl20wsmZhBtmZpcsR0tOfquwb8g==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "name": "@wolfy1339/lru-cache", + "version": "11.0.2-patch.1", + "resolved": "https://registry.npmjs.org/@wolfy1339/lru-cache/-/lru-cache-11.0.2-patch.1.tgz", + "integrity": "sha512-BgYZfL2ADCXKOw2wJtkM3slhHotawWkgIRRxq4wEybnZQPjvAp71SPX35xepMykTw8gXlzWcWPTY31hlbnRsDA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "18 >=18.20 || 20 || >=22" + } + }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memoize-weak": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/memoize-weak/-/memoize-weak-1.0.2.tgz", + "integrity": "sha512-gj39xkrjEw7nCn4nJ1M5ms6+MyMlyiGmttzsqAUsAKn6bYKwuTHh/AO3cKPF8IBrTIYTxb0wWXFs3E//Y8VoWQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/msgpackr": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/murmurhash3js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/murmurhash3js/-/murmurhash3js-3.0.1.tgz", + "integrity": "sha512-KL8QYUaxq7kUbcl0Yto51rMcYt7E/4N4BG3/c96Iqw1PQrTRspu8Cpx4TZ4Nunib1d4bEkIH3gjCYlP2RLBdow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abort-controller": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/oauth4webapi": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.3.0.tgz", + "integrity": "sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/octokit": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/octokit/-/octokit-3.1.2.tgz", + "integrity": "sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/app": "^14.0.2", + "@octokit/core": "^5.0.0", + "@octokit/oauth-app": "^6.0.0", + "@octokit/plugin-paginate-graphql": "^4.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0", + "@octokit/plugin-retry": "^6.0.0", + "@octokit/plugin-throttling": "^8.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", + "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", + "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.50.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz", + "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.0.tgz", + "integrity": "sha512-7hsAZ4xGXl4MW+OKEWCnF6T5jqBw80/EE9aXg1r2yyn1RsVEU8EtKXbijEODa+rg7iih4bKf7vlvTGYR4CnPNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-safe-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", + "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/posthog-node": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.8.1.tgz", + "integrity": "sha512-ApMEC1+DbctP/88+VhaCl8SRKpIoReibMf7Mb3rxw3yMthr1rKaM4opbHdZJ0buLhwS5zX8B2ckqLjpwpSjRPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.7.4" + }, + "engines": { + "node": ">=15.0.0" + } + }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-prisma": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-prisma/-/prettier-plugin-prisma-5.0.0.tgz", + "integrity": "sha512-jTJV04D9+yF7ziOOMs7CJe4ijgAH7DEGjt0SAWAToGNRy1H6BEhvcKA2UQH6gC6KVW5zeeOSAvsoiDDTt9oKXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@prisma/prisma-schema-wasm": "4.17.0-26.6b0aef69b7cdfc787f822ecd7cdc76d5f1991584" + }, + "engines": { + "node": ">=14", + "npm": ">=8" + }, + "peerDependencies": { + "prettier": ">=2 || >=3" + } + }, + "node_modules/prettier-plugin-svelte": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.3.tgz", + "integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" + } + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "node_modules/prisma/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-info": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redis-info/-/redis-info-3.1.0.tgz", + "integrity": "sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.11" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz", + "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.34.9", + "@rollup/rollup-android-arm64": "4.34.9", + "@rollup/rollup-darwin-arm64": "4.34.9", + "@rollup/rollup-darwin-x64": "4.34.9", + "@rollup/rollup-freebsd-arm64": "4.34.9", + "@rollup/rollup-freebsd-x64": "4.34.9", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", + "@rollup/rollup-linux-arm-musleabihf": "4.34.9", + "@rollup/rollup-linux-arm64-gnu": "4.34.9", + "@rollup/rollup-linux-arm64-musl": "4.34.9", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", + "@rollup/rollup-linux-riscv64-gnu": "4.34.9", + "@rollup/rollup-linux-s390x-gnu": "4.34.9", + "@rollup/rollup-linux-x64-gnu": "4.34.9", + "@rollup/rollup-linux-x64-musl": "4.34.9", + "@rollup/rollup-win32-arm64-msvc": "4.34.9", + "@rollup/rollup-win32-ia32-msvc": "4.34.9", + "@rollup/rollup-win32-x64-msvc": "4.34.9", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sil.appbuilder.portal.common": { + "resolved": "common", + "link": true + }, + "node_modules/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/solid-js": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.6.12.tgz", + "integrity": "sha512-JFqRobfG3q5r1l4RYVOAukk6+FWtHpXGIjgh/GEsHKweN/kK+iHOtzUALE6+P5t/jIcSNeGiVitX8gmJg+cYvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.1.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.1.tgz", + "integrity": "sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.22.1.tgz", + "integrity": "sha512-b9DQGnfrZc+km4u3j6qkEY87pYe23yfgBT03CkeBlYST+Wzij7ut1o0BSoQ+UmiyAO1nPh9DMJJCoGDdUOrunw==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "acorn": "^8.12.1", + "aria-query": "^5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "esm-env": "^1.2.1", + "esrap": "^1.4.3", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.4.tgz", + "integrity": "sha512-v0j7yLbT29MezzaQJPEDwksybTE2Ups9rUxEXy92T06TiA0cbqcO8wAOwNUVkFW6B0hsYHA+oAX3BS8b/2oHtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/svelte-eslint-parser": { + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", + "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "postcss": "^8.4.39", + "postcss-scss": "^4.0.9" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + } + } + }, + "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-flatpickr-plus": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/svelte-flatpickr-plus/-/svelte-flatpickr-plus-2.0.6.tgz", + "integrity": "sha512-5n2GogRoZytnzRy418Wkd4Cl+CzfiEaimKSeSsKt+la4BOoRn3IXDPuyaXQwLjHd6038eWQHiQuU4T/LXWWhGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatpickr_plus": "^1.1.20", + "postcss": "^8.4.49", + "postcss-import": "^16.1.0", + "svelte-preprocess": "^6.0.3" + }, + "peerDependencies": { + "flatpickr_plus": "^1.0.0", + "svelte": "^5.0.0" + } + }, + "node_modules/svelte-preprocess": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-6.0.3.tgz", + "integrity": "sha512-PLG2k05qHdhmRG7zR/dyo5qKvakhm8IJ+hD2eFRQmMLHp7X3eJnjeupUtvuRpbNiF31RjVw45W+abDwHEmP5OA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": ">=3", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": ">=0.55", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.100 || ^5.0.0", + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/svelte/node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/sveltekit-superforms": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/sveltekit-superforms/-/sveltekit-superforms-2.23.1.tgz", + "integrity": "sha512-SPj5ac4SMg8SPyP0Zi3ynwXJa7r9U1CTyn+YSyck67zLsjt367Sro4SZnl3yASrLd5kJ6Y57cgIdYJ2aWNArXw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ciscoheat" + }, + { + "type": "ko-fi", + "url": "https://ko-fi.com/ciscoheat" + }, + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=NY7F5ALHHSVQS" + } + ], + "license": "MIT", + "dependencies": { + "devalue": "^5.1.1", + "memoize-weak": "^1.0.2", + "ts-deepmerge": "^7.0.2" + }, + "optionalDependencies": { + "@exodus/schemasafe": "^1.3.0", + "@gcornut/valibot-json-schema": "^0.31.0", + "@sinclair/typebox": "^0.34.14", + "@typeschema/class-validator": "^0.3.0", + "@vinejs/vine": "^3.0.0", + "arktype": "^2.0.0", + "class-validator": "^0.14.1", + "effect": "^3.12.5", + "joi": "^17.13.3", + "json-schema-to-ts": "^3.1.1", + "superstruct": "^2.0.2", + "valibot": "1.0.0-beta.11", + "yup": "^1.6.1", + "zod": "^3.24.1", + "zod-to-json-schema": "^3.24.1" + }, + "peerDependencies": { + "@exodus/schemasafe": "^1.3.0", + "@sinclair/typebox": "^0.34.9", + "@sveltejs/kit": "1.x || 2.x", + "@typeschema/class-validator": "^0.3.0", + "@vinejs/vine": "^1.8.0 || ^2.0.0 || ^3.0.0", + "arktype": ">=2.0.0-rc.23", + "class-validator": "^0.14.1", + "effect": "^3.10.0", + "joi": "^17.13.1", + "superstruct": "^2.0.2", + "svelte": "3.x || 4.x || >=5.0.0-next.51", + "valibot": ">=1.0.0-beta.3", + "yup": "^1.4.0", + "zod": "^3.24.1" + }, + "peerDependenciesMeta": { + "@exodus/schemasafe": { + "optional": true + }, + "@sinclair/typebox": { + "optional": true + }, + "@typeschema/class-validator": { + "optional": true + }, + "@vinejs/vine": { + "optional": true + }, + "arktype": { + "optional": true + }, + "class-validator": { + "optional": true + }, + "effect": { + "optional": true + }, + "joi": { + "optional": true + }, + "superstruct": { + "optional": true + }, + "valibot": { + "optional": true + }, + "yup": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/sveltekit-superforms/node_modules/@sinclair/typebox": { + "version": "0.34.28", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.28.tgz", + "integrity": "sha512-e2B9vmvaa5ym5hWgCHw5CstP54au6AOLXrhZErLsOyyRzuWJtXl/8TszKtc5x8rw/b+oY7HKS9m9iRI53RK0WQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/sveltekit-superforms/node_modules/devalue": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", + "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/sveltekit-superforms/node_modules/valibot": { + "version": "1.0.0-beta.11", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.0.0-beta.11.tgz", + "integrity": "sha512-Ztl5Iks1Ql7Z6CwkS5oyqguN3G8tmUiNlsHpqbDt6DLMpm+eu+n8Q7f921gI3uHvNZ8xDVkd4cEJP5t+lELOfw==", + "dev": true, + "license": "MIT", + "optional": true, + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/svelvet": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/svelvet/-/svelvet-10.0.2.tgz", + "integrity": "sha512-CSxAyQ9xF/Qq3K0xsL/gy9u9UpC4DVFUUbq3awpQ9Dic4EGJ40qAhlcCKBBAb3Kul6hlks2bzlyBmNby8CfpDg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "svelte": ">=3.59.2 || ^4.0.0" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/tailwindcss/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tailwindcss/node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/tailwindcss/node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ts-api-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.1.tgz", + "integrity": "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-deepmerge": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-7.0.2.tgz", + "integrity": "sha512-akcpDTPuez4xzULo5NwuoKwYRtjQJ9eoNfBACiBMaXwNAx7B1PKfe5wqUFJuW5uKzQ68YjDFwPaWHDG1KnFGsA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14.13.1" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "optional": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/universal-github-app-jwt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/universal-github-app-jwt/-/universal-github-app-jwt-1.2.0.tgz", + "integrity": "sha512-dncpMpnsKBk0eetwfN8D8OUHGfiDhhJ+mtsbMl+7PfW7mYjiH8LIcqRmYMtzYLgSh47HjfdBtrBwIQ/gizKR3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "^9.0.0", + "jsonwebtoken": "^9.0.2" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/valibot": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.0.0-rc.3.tgz", + "integrity": "sha512-LT0REa7Iqx4QGcaHLiTiTkcmJqJ9QdpOy89HALFFBJgejTS64GQFRIbDF7e4f6pauQbo/myfKGmWXCLhMeM6+g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/validator": { + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", + "integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitefu": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", + "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz", + "integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "3.0.7", + "@vitest/mocker": "3.0.7", + "@vitest/pretty-format": "^3.0.7", + "@vitest/runner": "3.0.7", + "@vitest/snapshot": "3.0.7", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.7", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.7", + "@vitest/ui": "3.0.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xstate": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.18.2.tgz", + "integrity": "sha512-hab5VOe29D0agy8/7dH1lGw+7kilRQyXwpaChoMu4fe6rDP+nsHYhDYKfS2O4iXE7myA98TW6qMEudj/8NXEkA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "devOptional": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yup": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz", + "integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", + "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", + "license": "MIT" + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "dev": true, + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz", + "integrity": "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==", + "dev": true, + "license": "ISC", + "optional": true, + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/source/SIL.AppBuilder.Portal/package.json b/source/SIL.AppBuilder.Portal/package.json new file mode 100644 index 0000000000..e98b98d1af --- /dev/null +++ b/source/SIL.AppBuilder.Portal/package.json @@ -0,0 +1,71 @@ +{ + "name": "sil.appbuilder.portal", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "test": "npm run test:integration && npm run test:unit", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --plugin-search-dir . --check . && eslint .", + "format": "prettier --plugin-search-dir . --write .", + "test:integration": "playwright test", + "test:unit": "vitest", + "prepare": "svelte-kit sync" + }, + "prisma": { + "seed": "ts-node --esm prisma/seed.ts", + "schema": "common/prisma/schema.prisma" + }, + "devDependencies": { + "@auth/core": "^0.37.4", + "@iconify/svelte": "^4.2.0", + "@inlang/paraglide-js": "1.11.8", + "@inlang/paraglide-sveltekit": "^0.15.5", + "@playwright/test": "^1.50.0", + "@sveltejs/adapter-node": "^5.2.12", + "@sveltejs/kit": "^2.16.1", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@types/express": "^5.0.0", + "@types/node": "^22.10.10", + "@typescript-eslint/eslint-plugin": "^8.21.0", + "@typescript-eslint/parser": "^8.21.0", + "@vvo/tzdb": "^6.159.0", + "autoprefixer": "^10.4.20", + "commander": "^13.1.0", + "daisyui": "^4.12.23", + "eslint": "^9.19.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-svelte": "^2.46.1", + "fuse.js": "^7.0.0", + "postcss": "^8.5.1", + "prettier": "^3.4.2", + "prettier-plugin-prisma": "^5.0.0", + "prettier-plugin-svelte": "^3.3.3", + "svelte": "^5.19.2", + "svelte-check": "^4.1.4", + "svelte-flatpickr-plus": "^2.0.6", + "sveltekit-superforms": "^2.23.1", + "svelvet": "^10.0.2", + "tailwindcss": "^3.4.17", + "ts-node": "^10.9.2", + "tslib": "^2.8.1", + "typescript": "^5.7.3", + "valibot": "^1.0.0-beta.14", + "vite": "^6.0.11", + "vitest": "^3.0.4" + }, + "type": "module", + "volta": { + "node": "22.11.0" + }, + "dependencies": { + "@auth/express": "^0.8.4", + "@auth/sveltekit": "^1.7.4", + "@bull-board/express": "^6.7.1", + "express": "^4.21.2", + "sil.appbuilder.portal.common": "file:common" + } +} diff --git a/source/SIL.AppBuilder.Portal/playwright.config.ts b/source/SIL.AppBuilder.Portal/playwright.config.ts new file mode 100644 index 0000000000..962f19c38a --- /dev/null +++ b/source/SIL.AppBuilder.Portal/playwright.config.ts @@ -0,0 +1,12 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + webServer: { + command: 'npm run build && npm run preview', + port: 4173 + }, + testDir: 'tests', + testMatch: /(.+\.)?(test|spec)\.[jt]s/ +}; + +export default config; diff --git a/source/SIL.AppBuilder.Portal/postcss.config.js b/source/SIL.AppBuilder.Portal/postcss.config.js new file mode 100644 index 0000000000..ba80730477 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/source/SIL.AppBuilder.Portal/project.inlang/.gitignore b/source/SIL.AppBuilder.Portal/project.inlang/.gitignore new file mode 100644 index 0000000000..5e46596759 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/project.inlang/.gitignore @@ -0,0 +1 @@ +cache \ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/project.inlang/project_id b/source/SIL.AppBuilder.Portal/project.inlang/project_id new file mode 100644 index 0000000000..6764fb663d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/project.inlang/project_id @@ -0,0 +1 @@ +a2b64a8bf83ffe05b47903bba26eb1467448ecfade2587523f40a8340dfc577f \ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/project.inlang/settings.json b/source/SIL.AppBuilder.Portal/project.inlang/settings.json new file mode 100644 index 0000000000..78b4da244e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/project.inlang/settings.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "sourceLanguageTag": "en-us", + "languageTags": ["en-us", "es-419", "fr-FR"], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-missing-translation@latest/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@latest/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@latest/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-i18next@latest/dist/index.js" + ], + "plugin.inlang.i18next": { + "pathPattern": "./src/lib/locales/{languageTag}.json" + } +} diff --git a/source/SIL.AppBuilder.Portal/scriptoria-logo.svg b/source/SIL.AppBuilder.Portal/scriptoria-logo.svg new file mode 100644 index 0000000000..68d3da1d2d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/scriptoria-logo.svg @@ -0,0 +1,43 @@ + + + + diff --git a/source/SIL.AppBuilder.Portal/src/app.css b/source/SIL.AppBuilder.Portal/src/app.css new file mode 100644 index 0000000000..eb5d430b79 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/app.css @@ -0,0 +1,25 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +h1 { + @apply text-2xl font-bold p-8; +} + +h2 { + @apply text-xl font-bold p-4; +} + +h3 { + @apply text-lg font-bold p-2; +} + +.dropdown-content { + box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); +} +a.link { + color: #55f; +} +* { + font-size: 15px; +} diff --git a/source/SIL.AppBuilder.Portal/src/app.d.ts b/source/SIL.AppBuilder.Portal/src/app.d.ts new file mode 100644 index 0000000000..2186b19f8f --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/app.d.ts @@ -0,0 +1,16 @@ +import type { AvailableLanguageTag } from '../../lib/paraglide/runtime'; +import type { ParaglideLocals } from '@inlang/paraglide-sveltekit'; +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + interface Locals { + paraglide: ParaglideLocals; + } + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/source/SIL.AppBuilder.Portal/src/app.html b/source/SIL.AppBuilder.Portal/src/app.html new file mode 100644 index 0000000000..39a76708c0 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/app.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/source/SIL.AppBuilder.Portal/src/auth.ts b/source/SIL.AppBuilder.Portal/src/auth.ts new file mode 100644 index 0000000000..7fefed206e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/auth.ts @@ -0,0 +1,226 @@ +import { checkInviteErrors } from '$lib/organizationInvites'; +import { isAdmin, isAdminForOrg, isSuperAdmin } from '$lib/utils'; +import type { Session } from '@auth/express'; +import { SvelteKitAuth, type DefaultSession, type SvelteKitAuthConfig } from '@auth/sveltekit'; +import Auth0Provider from '@auth/sveltekit/providers/auth0'; +import { error, redirect, type Handle } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { verifyCanViewAndEdit } from './lib/projects/common.server'; + +declare module '@auth/sveltekit' { + interface Session { + user: { + userId: number; + /** [organizationId, RoleId][]*/ + roles: [number, number][]; + } & DefaultSession['user']; + } +} + +// Stupidly hacky way to get access to the request data from the auth callbacks +// This is a global variable that is set in a separate hook handle +let currentInviteToken: string | null = null; +let tokenStatus: TokenStatus | null = null; +enum TokenStatus { + Absent, + Valid, + Invalid +} + +const config: SvelteKitAuthConfig = { + trustHost: true, + providers: [ + Auth0Provider({ + id: 'auth0', + name: 'Auth0', + clientId: import.meta.env.VITE_AUTH0_CLIENT_ID, + clientSecret: import.meta.env.VITE_AUTH0_CLIENT_SECRET, + issuer: `https://${import.meta.env.VITE_AUTH0_DOMAIN}/`, + wellKnown: `https://${import.meta.env.VITE_AUTH0_DOMAIN}/.well-known/openid-configuration` + }) + ], + secret: import.meta.env.VITE_AUTH0_SECRET, + // debug: true, + session: { + maxAge: 60 * 60 * 24 // 24 hours + }, + callbacks: { + async signIn({ profile }) { + // The user must exist. Users can only be created initially through an organization invite. + // This means all users must have an organization. + + // 1. The user exists + // - There is an invite -> login, then redirect to /invitations/organization-membership + // - There is no invite -> login normally + // 2. The user does not exist and there is an invite + // - invite is invalid -> redirect to /invitations/organization-membership + // - invite is valid -> login, then redirect to /invitations/organization-membership + // 3. The user does not exist and there is no invite -> redirect to /login/no-organization + + if (!profile || !profile.sub) return false; + const user = await DatabaseWrites.utility.getUserIfExists(profile.sub); + if (user) { + return true; + } else { + if (tokenStatus === TokenStatus.Absent) return '/login/no-organization'; + if (tokenStatus === TokenStatus.Invalid) + return '/invitations/organization-membership?t=' + currentInviteToken; + // If there is a pending invitation, allow the login anyway and create the user account + if (tokenStatus === TokenStatus.Valid) { + await DatabaseWrites.utility.createUser(profile); + return true; + } + } + throw new Error('Invalid state'); + }, + async jwt({ profile, token }) { + // Called in two cases: + // a: client just logged in (new session): profile is passed and token is not (trigger == 'signIn') + // b: subsequent calls (during an existing session), token is passed and profile is not (trigger == 'update') + + // make sure to handle values that could change mid-session in both cases + // safest method is just handle such values in session below (see user.roles) + if (!profile) return token; + if (!profile.sub) throw new Error('No sub in profile'); + const dbUser = await DatabaseWrites.utility.getUserIfExists(profile.sub); + if (!dbUser) throw new Error('User not found'); + token.userId = dbUser.Id; + return token; + }, + async session({ session, token }) { + session.user.userId = token.userId as number; + const userRoles = await prisma.userRoles.findMany({ + where: { + UserId: token.userId as number + } + }); + session.user.roles = userRoles.map((role) => [role.OrganizationId, role.RoleId]); + return session; + } + } +}; +// Handles the /auth route, which is used to handle external auth0 authentication +export const { handle: authRouteHandle, signIn, signOut } = SvelteKitAuth(config); + +export const checkUserExistsHandle: Handle = async ({ event, resolve }) => { + // If the user does not exist in the database, invalidate the login and redirect to prevent unauthorized access + // This can happen when the user is deleted from the database but still has a valid session. + // This should only happen when a superadmin manually deletes a user but is particularly annoying in development + // The user should also be redirected if they are not a member of any organizations + // Finally, the user should be redirected if they are locked + const userId = (await event.locals.auth())?.user.userId; + if (!userId) { + // User has no session at all; allow normal events + return resolve(event); + } + const user = await prisma.users.findUnique({ + where: { + Id: userId + }, + include: { + OrganizationMemberships: true + } + }); + if (!user || (user.OrganizationMemberships.length === 0 && !event.cookies.get('inviteToken'))) { + event.cookies.set('authjs.session-token', '', { path: '/' }); + return redirect(302, '/login/no-organization'); + } + if (user.IsLocked) { + event.cookies.set('authjs.session-token', '', { path: '/' }); + return redirect(302, '/login/locked'); + } + return resolve(event); +}; + +// Handle organization invites +export const organizationInviteHandle: Handle = async ({ event, resolve }) => { + // Hacky solution to get the request object in the auth callbacks + // while making sure it only exists + currentInviteToken = event.cookies.get('inviteToken') ?? ''; + + // verify the token + if (currentInviteToken) { + const errors = await checkInviteErrors(currentInviteToken); + if (errors.error) tokenStatus = TokenStatus.Invalid; + else tokenStatus = TokenStatus.Valid; + } else { + tokenStatus = TokenStatus.Absent; + } + + const result = await resolve(event); + + currentInviteToken = null; + tokenStatus = null; + return result; +}; + +// Locks down the authenticated routes by redirecting to /login +// This guarantees a logged in user under (authenticated) but does not guarantee +// authorization to the route. Each page must manually check in +page.server.ts or here +export const localRouteHandle: Handle = async ({ event, resolve }) => { + if ( + !event.route.id?.startsWith('/(unauthenticated)') && + event.route.id !== '/' && + event.route.id !== null + ) { + const session = await event.locals.auth(); + if (!session) return redirect(302, '/login'); + if (!(await validateRouteForAuthenticatedUser(session, event.route.id, event.params))) + return error(403); + } + return resolve(event); +}; + +async function validateRouteForAuthenticatedUser( + session: Session, + route: string, + params: Partial> +): Promise { + const path = route.split('/').filter((r) => !!r); + // Only guarding authenticated routes + if (path[0] === '(authenticated)') { + if (path[1] === 'admin' || path[1] === 'workflow-instances') + return isSuperAdmin(session?.user?.roles); + else if (path[1] === 'directory' || path[1] === 'open-source') + // Always allowed. Open pages + return true; + else if (path[1] === 'organizations') { + // Must be org admin or super admin for some organization + if (!isAdmin(session?.user?.roles)) return false; + // Must be org admin or super admin for this organization + if (params.id) + return isAdminForOrg(parseInt(params.id!), session?.user?.roles); + return true; + } else if (path[1] === 'products') { + // TODO not sure, probably based on ownership of the project + const projectId = ( + await prisma.products.findFirst({ + where: { + Id: params.id + } + }) + )?.ProjectId; + if (!projectId) return false; + return verifyCanViewAndEdit(session, projectId); + } else if (path[1] === 'projects') { + if (path[2] === '[filter=projectSelector]') return true; + // TODO: what are the conditions for viewing a project + // I imagine either own it or be organization admin + else if (path[2] === '[id=idNumber]') { + return await verifyCanViewAndEdit(session, parseInt(params.id!)); + } + return true; + } else if (path[1] === 'tasks') { + // Own task list always allowed, and specific products checked manually + return true; + } else if (path[1] === 'users') { + // Checked manually in the users route + return true; + } else { + // Unknown route. We'll assume it's a legal route + return true; + } + } else { + return true; + } +} diff --git a/source/SIL.AppBuilder.Portal/src/hooks.server.ts b/source/SIL.AppBuilder.Portal/src/hooks.server.ts new file mode 100644 index 0000000000..2fde2e27a6 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/hooks.server.ts @@ -0,0 +1,18 @@ +// hooks.server.ts +import { i18n } from '$lib/i18n'; +import type { Handle } from '@sveltejs/kit'; +import { sequence } from '@sveltejs/kit/hooks'; +import { + authRouteHandle, + checkUserExistsHandle, + localRouteHandle, + organizationInviteHandle +} from './auth'; + +export const handle: Handle = sequence( + i18n.handle(), + organizationInviteHandle, + authRouteHandle, + checkUserExistsHandle, + localRouteHandle +); diff --git a/source/SIL.AppBuilder.Portal/src/hooks.ts b/source/SIL.AppBuilder.Portal/src/hooks.ts new file mode 100644 index 0000000000..38908d34ff --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/hooks.ts @@ -0,0 +1,4 @@ +// file initialized by the Paraglide-SvelteKit CLI - Feel free to edit it +import { i18n } from '$lib/i18n'; + +export const reroute = i18n.reroute(); diff --git a/source/SIL.AppBuilder.Portal/src/index.test.ts b/source/SIL.AppBuilder.Portal/src/index.test.ts new file mode 100644 index 0000000000..964d287251 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/index.test.ts @@ -0,0 +1,7 @@ +import { describe, it, expect } from 'vitest'; + +describe('sum test', () => { + it('adds 1 + 2 to equal 3', () => { + expect(1 + 2).toBe(3); + }); +}); diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/DateRangePicker.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/DateRangePicker.svelte new file mode 100644 index 0000000000..e1ef1d528a --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/DateRangePicker.svelte @@ -0,0 +1,63 @@ + + +
+ + (chosenDates = [ + v[0], + // Add one day to include the final day + v[1] ? new Date(v[1].getTime() + 1000 * 60 * 60 * 24) : null + ]) + }} + class="input input-bordered w-full min-w-40" + {placeholder} + /> +
+ + diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/Dropdown.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/Dropdown.svelte new file mode 100644 index 0000000000..507ff8ae6e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/Dropdown.svelte @@ -0,0 +1,34 @@ + + + + + diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/IconContainer.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/IconContainer.svelte new file mode 100644 index 0000000000..fc7c27d209 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/IconContainer.svelte @@ -0,0 +1,27 @@ + + +
+ +
diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/LanguageCodeTypeahead.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/LanguageCodeTypeahead.svelte new file mode 100644 index 0000000000..a2c1a4c31e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/LanguageCodeTypeahead.svelte @@ -0,0 +1,153 @@ + + +{#snippet colorValueForKeyMatch( + obj: Record, + key: string, + matches?: readonly FuseResultMatch[] +)} + {@const value = obj[key]} + {#if !matches} + {value} + {:else} + {@const matchList = matches.filter((match) => match.key === key)} + {#if !matchList.length} + {value} + {:else} + {@const hasMultiCharMatch = matches.some((match) => + // Note: match indices are inclusive (e.x. a match of indices [0, 2] indicates the + // first three chars of a string) so we use +1 a lot to get the length of the match + match.indices.some(([x, y]) => y - x + 1 > 2) + )} +
+ {#each parseMatches(value, matchList, hasMultiCharMatch) as match} + {#if match.h} + {match.v} + {:else} + {match.v} + {/if} + {/each} +
+ {/if} + {/if} +{/snippet} + + fuzzySearch.search(search).slice(0, 5)} + classes="pr-20 {inputClasses}" + bind:search={langCode} + onItemClicked={(item) => { + langCode = item.item.tag; + onLangCodeSelected?.(langCode); + }} + {dropdownClasses} + bind:inputElement={typeaheadInput} +> + + + + {#snippet custom()} + typeaheadInput?.focus()}> + {langtagList.find((l) => l.tag === langCode)?.name ?? ''} + + {/snippet} + {#snippet listElement({ item })} +
+ + + + {#if item.item.localname} + + {@render colorValueForKeyMatch(item.item, 'localname', item.matches)} + +
+ + {@render colorValueForKeyMatch(item.item, 'name', item.matches)} + + {:else} + + {@render colorValueForKeyMatch(item.item, 'name', item.matches)} + + {/if} +
+ + + {@render colorValueForKeyMatch(item.item, 'tag', item.matches)} + +
+ {m.localePicker_code()} +
+
+ {/snippet} +
diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/LanguageSelector.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/LanguageSelector.svelte new file mode 100644 index 0000000000..a22316de3b --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/LanguageSelector.svelte @@ -0,0 +1,44 @@ + + +{#key languageTag()} + +{/key} diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/OrganizationSelector.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/OrganizationSelector.svelte new file mode 100644 index 0000000000..799cfdbe02 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/OrganizationSelector.svelte @@ -0,0 +1,53 @@ + + +
+ + + + + + + + + + + {#each organizations as org} + onSelect(org.Id)} + > + + + + + {/each} + +
OrganizationOwner
+ {#if org.LogoUrl} + Logo + {:else} +
+
+
+ {/if} + + {org.Name} + +
+ {org.Owner.Name} +
+
diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/Pagination.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/Pagination.svelte new file mode 100644 index 0000000000..4af621860e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/Pagination.svelte @@ -0,0 +1,76 @@ + + +{#snippet button(index: number)} + +{/snippet} + +
+ {#if pageCount > 1} +
+ + {@render button(0)} + {#if collapse} + {#if page > 3} + + {:else} + {@render button(1)} + {/if} + {#each Array.from({ length: 3 }) as _, i} + {@render button(index(i, page))} + {/each} + {#if page < pageCount - 4} + + {:else} + {@render button(pageCount - 2)} + {/if} + {:else} + {#each Array.from({ length: pageCount - 2 }) as _, i} + {@render button(i + 1)} + {/each} + {/if} + {@render button(pageCount - 1)} + +
+ {/if} +
+
+ {m.common_total({ total })} +
+
 
+ +
+
diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/SearchBar.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/SearchBar.svelte new file mode 100644 index 0000000000..42a405ea36 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/SearchBar.svelte @@ -0,0 +1,31 @@ + + +
+ +
+ + diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/SortTable.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/SortTable.svelte new file mode 100644 index 0000000000..7fa40db899 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/SortTable.svelte @@ -0,0 +1,177 @@ + + + +
+ + + + {#each columns as c} + + {/each} + + + + {#each data as d} + { + onRowClick?.(d, e); + }} + > + {#each columns as c} + + {/each} + + {/each} + +
{ + if (c.sortable) { + sortColByDirection(c); + } + }} + > + + +
+ {#if c.render} + {@html c.render(c.data(d))} + {:else} + {c.data(d)} + {/if} +
+
+ + diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/Tooltip.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/Tooltip.svelte new file mode 100644 index 0000000000..6cd87c3860 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/Tooltip.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + + + diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/TypeaheadInput.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/TypeaheadInput.svelte new file mode 100644 index 0000000000..115b3fa2ea --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/TypeaheadInput.svelte @@ -0,0 +1,96 @@ + + +
+ (inputFocused = false)} + bind:this={inputElement} + {...inputElProps} + /> + {@render custom?.()} + {#if list.length} +
    + {#each list as item, i} +
  • selectItem(item)} + onmouseover={() => (selectedIndex = i)} + onfocus={() => (selectedIndex = i)} + > + {@render listElement?.({ item })} +
  • + {/each} +
+ {/if} +
+ + diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/settings/DataDisplayBox.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/settings/DataDisplayBox.svelte new file mode 100644 index 0000000000..235baacb69 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/settings/DataDisplayBox.svelte @@ -0,0 +1,46 @@ + + + +
+
+

{title}

+ {#if editable} + + {/if} + {#each fields as field} +

+ {m[field.key]()}: + {#if field.snippet} + {@render field.snippet(data)} + {:else} + {field.value ?? ''} + {/if} +

+ {/each} + {@render children?.()} +
+
diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/settings/LabeledFormInput.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/settings/LabeledFormInput.svelte new file mode 100644 index 0000000000..88acdbab84 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/settings/LabeledFormInput.svelte @@ -0,0 +1,27 @@ + + + +
+ +
diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/settings/MultiselectBox.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/settings/MultiselectBox.svelte new file mode 100644 index 0000000000..af81db4faf --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/settings/MultiselectBox.svelte @@ -0,0 +1,15 @@ + + +
+
+ {header} +
+ {@render children?.()} +
diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/settings/MultiselectBoxElement.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/settings/MultiselectBoxElement.svelte new file mode 100644 index 0000000000..194e0e4a10 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/settings/MultiselectBoxElement.svelte @@ -0,0 +1,23 @@ + + +
+ +
diff --git a/source/SIL.AppBuilder.Portal/src/lib/components/settings/TabbedMenu.svelte b/source/SIL.AppBuilder.Portal/src/lib/components/settings/TabbedMenu.svelte new file mode 100644 index 0000000000..948c587996 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/components/settings/TabbedMenu.svelte @@ -0,0 +1,108 @@ + + +
+
+
+ {#if title} + {@render title()} + {:else} +

{titleString}

+ {/if} +
+ +
+
+ + {menuItems.find((item) => + page.route.id?.split(routeId + '/')[1]?.startsWith(item.route) + )?.text} + + +
+
+ +
+ +
+
+
+ {@render children?.()} +
+
+
+
+ + diff --git a/source/SIL.AppBuilder.Portal/src/lib/i18n.ts b/source/SIL.AppBuilder.Portal/src/lib/i18n.ts new file mode 100644 index 0000000000..fec3f8dc90 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/i18n.ts @@ -0,0 +1,11 @@ +// file initialized by the Paraglide-SvelteKit CLI - Feel free to edit it +import { createI18n } from '@inlang/paraglide-sveltekit'; +import * as runtime from '$lib/paraglide/runtime.js'; +import * as m from './paraglide/messages'; + +export const i18n = createI18n(runtime); + +type ValidKey = { + [K in keyof T]: T[K] extends () => void ? K : never; +}[keyof T]; +export type ValidI13nKey = ValidKey; diff --git a/source/SIL.AppBuilder.Portal/src/lib/icons/ArrowBackIcon.svelte b/source/SIL.AppBuilder.Portal/src/lib/icons/ArrowBackIcon.svelte new file mode 100644 index 0000000000..16d573ed1d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/icons/ArrowBackIcon.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/source/SIL.AppBuilder.Portal/src/lib/icons/ArrowDownIcon.svelte b/source/SIL.AppBuilder.Portal/src/lib/icons/ArrowDownIcon.svelte new file mode 100644 index 0000000000..36aeaf3166 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/icons/ArrowDownIcon.svelte @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/lib/icons/ArrowUpIcon.svelte b/source/SIL.AppBuilder.Portal/src/lib/icons/ArrowUpIcon.svelte new file mode 100644 index 0000000000..cd71ac3a17 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/icons/ArrowUpIcon.svelte @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/lib/icons/HamburgerIcon.svelte b/source/SIL.AppBuilder.Portal/src/lib/icons/HamburgerIcon.svelte new file mode 100644 index 0000000000..bf8348e62d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/icons/HamburgerIcon.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/source/SIL.AppBuilder.Portal/src/lib/icons/LanguageIcon.svelte b/source/SIL.AppBuilder.Portal/src/lib/icons/LanguageIcon.svelte new file mode 100644 index 0000000000..7eba61c22f --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/icons/LanguageIcon.svelte @@ -0,0 +1,25 @@ + + + + + diff --git a/source/SIL.AppBuilder.Portal/src/lib/icons/ScriptoriaIcon.svelte b/source/SIL.AppBuilder.Portal/src/lib/icons/ScriptoriaIcon.svelte new file mode 100644 index 0000000000..087f8d109d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/icons/ScriptoriaIcon.svelte @@ -0,0 +1,35 @@ + + + + + + + + + diff --git a/source/SIL.AppBuilder.Portal/src/lib/icons/index.js b/source/SIL.AppBuilder.Portal/src/lib/icons/index.js new file mode 100644 index 0000000000..095abcfcf3 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/icons/index.js @@ -0,0 +1,7 @@ +import ArrowBackIcon from './ArrowBackIcon.svelte'; +import ArrowDownIcon from './ArrowDownIcon.svelte'; +import HamburgerIcon from './HamburgerIcon.svelte'; +import LanguageIcon from './LanguageIcon.svelte'; +import ArrowUpIcon from './ArrowUpIcon.svelte'; + +export { ArrowBackIcon, ArrowDownIcon, HamburgerIcon, LanguageIcon, ArrowUpIcon }; diff --git a/source/SIL.AppBuilder.Portal/src/lib/icons/productDefinitionIcon.ts b/source/SIL.AppBuilder.Portal/src/lib/icons/productDefinitionIcon.ts new file mode 100644 index 0000000000..89869c5236 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/icons/productDefinitionIcon.ts @@ -0,0 +1,17 @@ +const iconMap = { + android: 'flat-color-icons:android-os', + html: 'mdi:web', + pwa: 'mdi:web', + package: 'mdi:archive', + none: 'mdi:error-outline' +}; +// Not sure I like this, but it's implemented here as it was in S1. +// I would suggest having a productDefinition db field for what type this is +// TODO: icon colors? +export function getIcon(name: string) { + return iconMap[ + (Object.keys(iconMap).find((key) => + name.toLowerCase().includes(key) + ) as keyof typeof iconMap) ?? 'none' + ]; +} diff --git a/source/SIL.AppBuilder.Portal/src/lib/index.ts b/source/SIL.AppBuilder.Portal/src/lib/index.ts new file mode 100644 index 0000000000..856f2b6c38 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/source/SIL.AppBuilder.Portal/src/lib/locales/en-us.json b/source/SIL.AppBuilder.Portal/src/lib/locales/en-us.json new file mode 100644 index 0000000000..42d9f8a42b --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/locales/en-us.json @@ -0,0 +1,479 @@ +{ + "appName": "Scriptoria", + "welcome": "Welcome to Scriptoria", + "search": "Search", + "contactUs": "Contact Us", + "exampleForm": "Example Form", + "updated": "Updated!", + "opensource": "Open Source", + "home": "Home", + "tabAppName_zero": "Scriptoria", + "tabAppName_other": "({{count}}) Scriptoria", + "localePicker_code": "Code", + "localePicker_country": "Country", + "localePicker_other": "Other Names", + "localePicker_label": "Language", + "localePicker_placeholder": "Search by Language...", + "localePicker_region": "{num, plural, one {{Region}} other {Regions}}", + "localePicker_name": "{num, plural, one {{Name}} other {Names}}", + "localePicker_tag": "{num, plural, one {{Tag}} other {Tags}}", + "localePicker_variant": "{num, plural, one {{Variant}} other {Variants}}", + "localePicker_regionName": "Region Name", + "attributions_title": "Open Source Project used in Scriptoria", + "attributions_subtitle": "Libraries used by License", + "notifications_buildComplete": "The build for {{projectName}} has completed!", + "notifications_buildServerConnectionLost": "Connection to the build server has been lost!", + "common_search": "Search", + "common_noResults": "No Results...", + "common_change": "Change", + "common_cancel": "Cancel", + "common_save": "Save", + "common_add": "Add", + "common_name": "Name", + "common_clickToEdit": "Click to edit", + "common_none": "None", + "common_abbreviation": "Abbreviation", + "common_general": "General Information", + "common_notAvailable": "Not Available", + "common_archive": "Archive", + "common_reactivate": "Reactivate", + "common_build": "Build / Rebuild", + "common_rebuild": "Rebuild / Republish", + "common_updated": "Updated", + "common_continue": "Continue", + "common_error": "Error", + "common_workflow": "Workflow", + "common_default": "Default", + "common_total": "Total {{total}}", + "common_id": "Id", + "models_add": "Add {{name}}", + "models_edit": "Edit {{name}}", + "models_save": "Save {{name}}", + "models_createSuccess": "{{name}} was successfully created!", + "models_updateSuccess": "{{name}} was successfully updated!", + "auth_title": "Scriptoria", + "auth_signup": "Sign Up", + "auth_login": "Log In", + "directory_title_zero": "Project Directory", + "directory_title_other": "Project Directory ({{numProjects}})", + "directory_searchHelp": "This allows you to search over: \n• Project Name\n• Project Language \n• Owner Name\n• Organization Name\n• Group Name", + "directory_filters_dateRange": "Project Updated Date Between", + "header_myProfile": "My Profile", + "header_community": "Community", + "header_help": "Help", + "header_signOut": "Sign out", + "header_clearAll": "Clear All", + "header_emptyNotifications": "You have no notifications.", + "sidebar_myTasks_zero": "My Tasks", + "sidebar_myTasks_other": "My Tasks ({{count}})", + "sidebar_myProjects": "My Projects", + "sidebar_activeProjects": "Active Projects", + "sidebar_organizationProjects": "Organization Projects", + "sidebar_users": "Users", + "sidebar_organizationSettings": "Organization Settings", + "sidebar_adminSettings": "Admin", + "sidebar_projectDirectory": "Project Directory", + "sidebar_addProject": "Add Project", + "sidebar_importProjects": "Import Projects", + "sidebar_jobAdministration": "Job Administration", + "invitations_orgPrompt": "Like to sign up your organization?", + "invitations_missingTokenTitle": "Your invitation token is missing", + "invitations_missingTokenPrompt": "Please check the link and try again", + "invitations_orgInviteTitle": "You have been invited to create an organization!", + "invitations_orgName": "Organization Name", + "invitations_orgUrl": "Organization Website URL", + "invitations_orgSubmit": "Add Organization", + "newOrganization_title": "Add organization", + "org_allOrganizations": "All Organizations", + "org_createSuccess": "Organization created successfully!", + "org_settingsTitle": "Organization Settings", + "org_logoUrl": "Logo URL", + "org_noteLogUrl": "Note: For optimal quality use a square image", + "org_productsTitle": "Products and Publishing", + "org_makePrivateTitle": "Make Projects Public by Default", + "org_makePrivateDescription": "\n When a new project is created, it will be defaulted to Public.\n (Private projects cannot be viewed by anyone outside of your organization)", + "org_productSelectTitle": "Select all the products you would like to make available to your organization", + "org_noproducts": "No products available", + "org_storesTitle": "Stores", + "org_storeSelectTitle": "Select all the stores you would like to make available to your organization", + "org_nostores": "No stores available", + "org_navBasic": "Basic Info", + "org_navProducts": "Products & Publishing", + "org_navStores": "Stores", + "org_navGroups": "Groups", + "org_nameError": "Name cannot be empty", + "org_abbreviationError": "Abbreviation cannot be empty", + "org_groupCreated": "Group created", + "org_groupEdited": "Group edited", + "org_groupDeleted": "Group deleted", + "org_navInfrastructure": "Infrastructure", + "org_infrastructureTitle": "Infrastructure", + "org_useDefaultBuildEngineTitle": "Use Default Build Engine", + "org_groupsTitle": "Groups", + "org_noGroups": "Your organization has no groups", + "org_addGroupButton": "Add Group", + "org_basicTitle": "Basic Info", + "org_orgName": "Organization Name", + "org_save": "Save", + "org_buildEngineUrl": "Build Engine URL", + "org_buildEngineApiAccessToken": "Build Engine API Access Token", + "products_definition": "Useful files that result from a series of steps.", + "products_storeSelect": "Select a store for {{name}}", + "products_noStoresAvailable": "There are no stores available for the selected product. Please contact your organization administrator.", + "products_actions_executed": "{{actionName}} was successfully executed.", + "products_actions_cancelled": "{{actionName}} was successfully cancelled.", + "products_actions_rebuild": "Rebuild", + "products_actions_republish": "Republish", + "products_actions_cancel": "Cancel", + "products_actions_noneAvailable": "No actions are currently available for this product.", + "products_actions_bulkNotAllAllowed": "Not all of the selected projects' products can have the {{action}} action(s) applied.", + "products_actions_dispatched": "The {{action}} action has been dispatched.", + "products_actions_properties": "Properties", + "products_files_title": "Product Files", + "profile_title": "Profile", + "profile_pictureTitle": "Profile Picture", + "profile_general": "General", + "profile_generalInformation": "General Information", + "profile_updated": "Profile updated successfully!", + "profile_updatePicture": "Update your picture at Gravatar.com", + "profile_uploadPicture": "Upload new picture", + "profile_firstName": "First name", + "profile_lastName": "Last name", + "profile_name": "Display Name", + "profile_email": "Email", + "profile_phone": "Phone", + "profile_location": "Location", + "profile_timezone": "Timezone", + "profile_locale": "Locale", + "profile_timezonePlaceholder": "Select your timezone...", + "profile_notificationSettingsTitle": "Notification Settings", + "profile_optOutOfEmailOption": "I want to receive email notifications", + "profile_sshSettingsTitle": "Manage Personal SSH Keys", + "profile_sshKeyLabel": "Publishing SSH Key", + "profile_visibleProfile": "Profile visibility", + "profile_noPhone": "no phone added", + "profile_noTimezone": "(GMT-0)", + "profile_visibility_visible": "My profile information is publicly viewable", + "profile_visibility_restricted": "My profile information is restricted from the public", + "errors_generic": "An error occurred! {{errorMessage}}", + "errors_requiredField": "The {{field}} is required, but has no value", + "errors_notAuthorized": "You must login. This could be due to a prior login expiring.", + "errors_notFoundTitle": "Not Found!", + "errors_notFoundDescription": "Something went wrong and the page or resource could not be found!", + "errors_groupRequired": "You must be a member of at least one group.", + "errors_userForbidden": "An error occurred: Forbidden. Please contact your organization administrator.", + "errors_friendlyForbidden": "You are not allowed to do that. Please contact your organization administrator", + "errors_forbiddenForAllOrgs": "You are not allowed to do that. Please select an organization", + "errors_notAMemberOfOrg": "You are not a member of that organization", + "errors_notactiveUser": "User is not Active", + "errors_notactiveUserText": "\n This user is currently set to not active.\n
\n Please contact your organization administrator to discuss the reason for the status change.\n ", + "errors_orgMembershipRequired": "Organization Membership is Required", + "errors_orgMembershipRequiredText": "\n In order to use Scriptoria, you must be a member of at least one organization.\n
\n Please contact your organization administrator to discuss receiving an invite to\n an organization on Scriptoria.\n ", + "errors_orgMembershipChanged": "Organization Membership has Changed", + "errors_orgMembershipChangedText": "\n Your organization membership has changed.\n
\n This can take a few moments to process. Please sign out, wait a minute, and then sign in again for the change to take effect.\n ", + "errors_orgMustBeSelected": "An organization must be selected to view this page", + "errors_verifyEmailTitle": "Please verify your email address", + "errors_verifyEmailDescription": "\n In order to use Scriptoria, we need you to verify your email address.\n
\n Please log into your email and clik the verification link. ", + "errors_invalidProjectSelection": "No valid products available for selected project(s)", + "tasks_title": "My Tasks", + "tasks_project": "Project", + "tasks_product": "Product", + "tasks_assignedTo": "Assigned To", + "tasks_status": "Status", + "tasks_waitTime": "Wait Time", + "tasks_unclaimed": "[unclaimed]", + "tasks_noTasksTitle": "No tasks are assigned to you.", + "tasks_noTasksDescription": "Tasks that require your attention will appear here.", + "tasks_reassign": "Reassign", + "tasks_waiting": "Waiting {{waitTime}}", + "tasks_forNames": "for {{allowedNames}} to {{activityName}}", + "tasks_scriptoria": "Scriptoria", + "tasks_storeLanguage": "Store Listing Language", + "tasks_appProjectURL": "App Project URL", + "tasks_files_buildId": "BuildId", + "tasks_files_link": "Link", + "tasks_files_fileId": "FileId", + "projects_noBuilds": "No Builds Yet", + "projects_latestBuild": "Latest Build ({{version}})", + "projects_buildPending": "Build Pending", + "projects_buildFailed": "Build Failed", + "projects_switcher_myProjects": "My Projects ({{numProjects}})", + "projects_switcher_archived": "Archived ({{numProjects}})", + "projects_switcher_dropdown_orgProjects": "Organization Projects", + "projects_switcher_dropdown_myProjects": "My Projects", + "projects_switcher_dropdown_activeProjects": "Active Projects", + "projects_switcher_dropdown_archived": "Archived Projects", + "projects_switcher_dropdown_all": "All Projects", + "projects_bulk_buildModal_title": "Perform Bulk Rebuild/Republish", + "productDefinitions_filterAllProjects": "All projects that contain...", + "projectImport_importFile": "Import JSON File", + "projectImport_createSuccess": "Project Import successfully created. You will receive an email when the import is completed.", + "project_title": "Project", + "project_organizationContact": "Organization Contact: ", + "project_projectOwner": "Project Owner: ", + "project_claimOwnership": "Claim Ownership", + "project_claimSuccess": "Succesfully Claimed", + "project_editProject": "Edit Project", + "project_newProject": "New Project", + "project_importProjects": "Import Projects", + "project_importProjectsHelp": "Import Projects Help", + "project_projectName": "Project Name", + "project_noAvailableGroups": "There are no available groups for the selected organization", + "project_projectDescription": "Description", + "project_projectGroup": "Project Group", + "project_languageCode": "Language Code", + "project_type": "Type", + "project_public": "Public", + "project_private": "Private", + "project_visibilityLabel": "Public", + "project_visibilityDescription": "If you make your project Private, it will not be visible in the directory or accessible by other organizations", + "project_createdOn": "Created", + "project_overview": "Overview", + "project_productFiles": "Product files", + "project_dropdown_transfer": "Transfer Ownership", + "project_dropdown_archive": "Archive", + "project_dropdown_reactivate": "Reactivate", + "project_dropdown_build": "Build", + "project_details_title": "Details", + "project_details_language": "Language", + "project_details_type": "Project Type", + "project_products_title": "Products", + "project_products_empty": "You have no products for this project.", + "project_products_addRemove": "Add or Remove Products", + "project_products_add": "Add Products", + "project_products_remove": "Remove Products", + "project_products_popup_addTitle": "Select Product to Add", + "project_products_popup_removeTitle": "Select Product to Remove", + "project_products_popup_done": "Done", + "project_products_popup_empty": "No products available", + "project_products_popup_details": "Details", + "project_products_popup_properties": "Publishing Properties", + "project_products_publications_channel": "Channel", + "project_products_publications_status": "Status", + "project_products_publications_date": "Publish Date", + "project_products_publications_url": "Publish Url", + "project_products_publications_succeeded": "Success", + "project_products_publications_failed": "Failure", + "project_products_publications_console": "Console Text", + "project_products_options_update": "update", + "project_products_options_publish": "publish", + "project_products_transitions_productDetails": "Product Details", + "project_products_transitions_storeName": "Store Name", + "project_products_transitions_state": "State", + "project_products_transitions_user": "User", + "project_products_transitions_command": "Command", + "project_products_transitions_comment": "Comment", + "project_products_transitions_date": "Date", + "project_products_transitions_transitionTypes_1": "Activity", + "project_products_transitions_transitionTypes_2": "{{workflowType}} Workflow Started", + "project_products_transitions_transitionTypes_3": "{{workflowType}} Workflow Completed", + "project_products_transitions_transitionTypes_4": "{{workflowType}} Workflow Cancelled", + "project_products_transitions_transitionTypes_5": "{{workflowType}} Project Access", + "project_products_properties_computeType": "Compute Type", + "project_products_properties_selectComputeType": "Select compute type", + "project_products_properties_small": "Small", + "project_products_properties_medium": "Medium", + "project_products_properties_title": "Product Publishing Properties", + "project_products_updated": "Updated", + "project_products_size": "Size", + "project_products_filename": "Filename", + "project_products_published": "Published", + "project_products_unpublished": "Unpublished", + "project_products_numArtifacts": "{amount, plural, =0 {No Product Files} other {{ amount } Product Files}}", + "project_products_noArtifacts": "No Product Files", + "project_products_rebuild": "rebuild", + "project_products_creationInProgress": "Pending: You cannot add a product until the project setup has been completed.", + "project_settings_title": "Settings", + "project_settings_automaticRebuild_title": "Automatic Rebuilds", + "project_settings_automaticRebuild_description": "When automatic rebuilds are on, Scriptoria will automatically rebuild your products when the input source is updated", + "project_settings_organizationDownloads_title": "Allow Other Organizations to download", + "project_settings_organizationDownloads_description": "When this setting is on, any Scriptoria User that is able to view your project in the Directory will be able to download the Product Files (the outputs of the products).", + "project_settings_visibility_title": "Public Visibility", + "project_settings_visibility_description": "Public Projects allow any Scriptoria User to view your project in the Directory", + "project_side_repositoryLocation": "Repository Location", + "project_side_organization": "Organization", + "project_side_organization_noChange": "Organization cannot be changed", + "project_side_projectOwner": "Project Owner", + "project_side_projectGroup": "Project Group", + "project_side_authors_title": "Authors", + "project_side_authors_add": "add author", + "project_side_authors_close": "close", + "project_side_authors_form_user": "user", + "project_side_authors_form_userError": "User cannot be empty", + "project_side_authors_form_submit": "Add Author", + "project_side_authors_empty": "No authors", + "project_side_reviewers_title": "Reviewers", + "project_side_reviewers_add": "add reviewer", + "project_side_reviewers_close": "close", + "project_side_reviewers_form_name": "name", + "project_side_reviewers_form_nameError": "Name cannot be empty", + "project_side_reviewers_form_email": "email", + "project_side_reviewers_form_emptyEmailError": "Email cannot be empty", + "project_side_reviewers_form_invalidEmailError": "Invalid email address", + "project_side_reviewers_form_submit": "Add Reviewer", + "project_side_reviewers_empty": "No reviewers", + "project_operations_archive_success": "Project archived", + "project_operations_archive_error": "There was an error trying to archive the project", + "project_operations_reactivate_success": "Project reactivated", + "project_operations_reactivate_error": "There was an error trying to reactivate the project", + "project_operations_automaticBuilds_on": "Automatic builds is ON", + "project_operations_automaticBuilds_off": "Automatic builds is OFF", + "project_operations_automaticBuilds_error": "Error: We could not change automatic build status", + "project_operations_allowDownloads_on": "Allow project download is ON", + "project_operations_allowDownloads_off": "Allow project download is OFF", + "project_operations_allowDownloads_error": "Error: We could not change Allow Download status", + "project_operations_isPublic_on": "Project visibility is set to Public", + "project_operations_isPublic_off": "Project visibility is set to Private", + "project_operations_isPublic_error": "Error: We could not change Project visibility", + "projectTable_columns_project": "Project", + "projectTable_columns_owner": "Owner", + "projectTable_columns_organization": "Organization", + "projectTable_columns_language": "Language", + "projectTable_columns_group": "Group", + "projectTable_columns_buildVersion": "Build Version", + "projectTable_columns_buildDate": "Build Date", + "projectTable_columns_createdOn": "Created On", + "projectTable_columns_updatedOn": "Updated On", + "projectTable_columns_activeSince": "Active Since", + "projectTable_empty": "No projects found", + "projectTable_products": "Products", + "projectTable_noProducts": "This project has no configured products", + "users_settingsTitle": "User Settings", + "users_userProfile": "User Profile", + "users_userGroups": "Group Memberships", + "users_userRoles": "Organization Roles", + "users_noneFound": "No users matching the selected criteria were found.", + "users_noRoles": "No roles assigned", + "users_roles_superAdmin": "Super Admin", + "users_roles_orgAdmin": "Organization Admin", + "users_roles_appBuilder": "App Builder", + "users_roles_author": "Author", + "users_organization_filter": "Filter organization", + "users_title": "Manage Users", + "users_addUser_placeholder": "Enter an email", + "users_addUser_button": "Add User to {{organization}}", + "users_addUser_modalTitle": "Add User to {{organization}}", + "users_addUser_modalAddButton": "Add", + "users_addUser_error": "No user was found with that email address. Please ask them to create an account.", + "users_table_columns_name": "Name", + "users_table_columns_role": "Role", + "users_table_columns_groups": "Groups", + "users_table_columns_active": "Active", + "users_operations_lock_success": "User is locked. A locked user will not be able to log in to Scriptoria.", + "users_operations_lock_error": "There was an error locking the user", + "users_operations_unlock_success": "User is active. The user will now be able to log in to Scriptoria.", + "users_operations_unlock_error": "there was an error activating the user", + "stores_name": "Store", + "stores_listTitle": "Stores", + "stores_attributes_name": "Name", + "stores_attributes_description": "Description", + "storeTypes_name": "Store Type", + "storeTypes_listTitle": "Store Types", + "admin_settings_title": "Admin settings", + "admin_settings_navigation_organizations": "Organizations", + "admin_settings_navigation_workflowdefinitions": "Workflow Definitions", + "admin_settings_navigation_productDefinitions": "Product Definitions", + "admin_settings_navigation_storeTypes": "Store Types", + "admin_settings_navigation_stores": "Stores", + "admin_settings_navigation_buildEngines": "Build Engine Statuses", + "admin_settings_organizations_title": "Organizations", + "admin_settings_organizations_name": "Organization Name", + "admin_settings_organizations_owner": "Owner", + "admin_settings_organizations_emptyOwner": "Need to select an owner for this organization", + "admin_settings_organizations_add": "Add Organization", + "admin_settings_organizations_addSuccess": "Organization added", + "admin_settings_organizations_edit": "Edit Organization", + "admin_settings_organizations_editSuccess": "Organization updated", + "admin_settings_organizations_websiteURL": "Website URL", + "admin_settings_organizations_buildEngineURL": "Build Engine URL", + "admin_settings_organizations_accessToken": "Build Engine API Access Token", + "admin_settings_organizations_logoURL": "Logo URL", + "admin_settings_organizations_publicByDefault": "Public By Default", + "admin_settings_organizations_publicByDefaultDescription": "Projects created under this organization are set to public by default", + "admin_settings_workflowDefinitions_title": "Workflow Definitions", + "admin_settings_workflowDefinitions_name": "Workflow Definition Name", + "admin_settings_workflowDefinitions_description": "Description", + "admin_settings_workflowDefinitions_workflowScheme": "Workflow Scheme", + "admin_settings_workflowDefinitions_workflowBusinessFlow": "Workflow Business Flow", + "admin_settings_workflowDefinitions_properties": "Properties", + "admin_settings_workflowDefinitions_add": "Add Workflow Definition", + "admin_settings_workflowDefinitions_edit": "Edit Workflow Definition", + "admin_settings_workflowDefinitions_storeType": "Store Type", + "admin_settings_workflowDefinitions_enabled": "Enabled", + "admin_settings_workflowDefinitions_enabledDescription": "Workflow definitions may not be used until enabled.", + "admin_settings_workflowDefinitions_workflowAdded": "Workflow Definition added", + "admin_settings_workflowDefinitions_workflowUpdated": "Workflow Definition updated", + "admin_settings_workflowDefinitions_emptyStoreType": "Need to select a store type for this workflow definition", + "admin_settings_workflowDefinitions_emptyWorkflowType": "Need to select a workflow type for this workflow definition", + "admin_settings_workflowDefinitions_workflowType": "Workflow Type", + "admin_settings_workflowDefinitions_workflowTypes_1": "Startup", + "admin_settings_workflowDefinitions_workflowTypes_2": "Rebuild", + "admin_settings_workflowDefinitions_workflowTypes_3": "Republish", + "admin_settings_workflowDefinitions_productType": "Product Type", + "admin_settings_workflowDefinitions_productType_assetPackage": "Asset Package", + "admin_settings_workflowDefinitions_productType_web": "Web", + "admin_settings_workflowDefinitions_options": "Options", + "admin_settings_workflowDefinitions_options_storeAccess": "Require an organization admin to access the GooglePlay developer console.", + "admin_settings_workflowDefinitions_options_approval": "Require approval by an organization admin before product is created.", + "admin_settings_workflowDefinitions_options_transferToAuthors": "Allow project owner to delegate configuration and product uploads to authors.", + "admin_settings_productDefinitions_title": "Product Definitions", + "admin_settings_productDefinitions_name": "Product definition Name", + "admin_settings_productDefinitions_add": "Add Product Definition", + "admin_settings_productDefinitions_edit": "Edit Product Definition", + "admin_settings_productDefinitions_description": "Description", + "admin_settings_productDefinitions_properties": "Properties", + "admin_settings_productDefinitions_type": "Application Type", + "admin_settings_productDefinitions_workflow": "Workflow", + "admin_settings_productDefinitions_republishWorkflow": "Republish Workflow", + "admin_settings_productDefinitions_rebuildWorkflow": "Rebuild Workflow", + "admin_settings_productDefinitions_addSuccess": "Product Definition added", + "admin_settings_productDefinitions_editSuccess": "Product Definition updated", + "admin_settings_productDefinitions_emptyName": "Name cannot be empty", + "admin_settings_productDefinitions_emptyType": "Need to select an Application Type", + "admin_settings_productDefinitions_emptyWorkflow": "Need to select a Workflow definition", + "admin_settings_productDefinitions_noWorkflow": "No Workflow Required", + "admin_settings_storeTypes_title": "Store Types", + "admin_settings_storeTypes_name": "Store Type Name", + "admin_settings_storeTypes_add": "Add Store Type", + "admin_settings_storeTypes_edit": "Edit Store Type", + "admin_settings_storeTypes_description": "Description", + "admin_settings_storeTypes_type": "Application Type", + "admin_settings_storeTypes_workflow": "Workflow", + "admin_settings_storeTypes_addSuccess": "Store Type added", + "admin_settings_storeTypes_editSuccess": "Store Type updated", + "admin_settings_storeTypes_emptyName": "Name cannot be empty", + "admin_settings_storeTypes_emptyType": "Need to select an Application Type", + "admin_settings_storeTypes_emptyWorkflow": "Need to select a Workflow definition", + "admin_settings_buildEngines_title": "Build Engine Statuses", + "admin_settings_buildEngines_accessToken": "Build Engine API Access Token", + "admin_settings_buildEngines_websiteURL": "Website URL", + "admin_settings_buildEngines_connected": "Connected", + "admin_settings_buildEngines_disconnected": "Disconnected", + "admin_settings_buildEngines_status": "Status", + "admin_settings_buildEngines_lastUpdated": "Last update", + "admin_settings_jobAdministration_title": "Job Administration", + "admin_settings_jobAdministration_test_instructions": "Choose how long the test task should take", + "admin_settings_jobAdministration_test_seconds": "seconds", + "admin_settings_jobAdministration_test_add": "Add Test task", + "admin_settings_jobAdministration_list": "View Job Statuses", + "workflowInstances_title": "Workflow Instances", + "workflowInstances_empty": "No workflow instances found", + "workflowInstances_jump": "Jump State to {{state}}", + "workflowInstances_filters_dateRange": "Last Updated Date Between", + "organizationMembership_invite_error_expired": "Invitation has expired", + "organizationMembership_invite_error_redeemed": "Invitation has already been redeemed", + "organizationMembership_invite_error_notFound": "Invitation was not found", + "organizationMembership_invite_error_unexpected": "Unexpected error occured: {{response}}", + "organizationMembership_invite_error_invalidResponse": "We've received an invalid response... please refresh the page to try again.", + "organizationMembership_invite_create_success": "Invite sent to {{email}}.", + "organizationMembership_invite_create_error": "Error occured while trying to invite user.", + "organizationMembership_invite_create_inviteUserButtonTitle": "Invite User", + "organizationMembership_invite_create_inviteUserModalTitle": "Invite User", + "organizationMembership_invite_create_emailInputPlaceholder": "Enter email address", + "organizationMembership_invite_create_sendInviteButton": "Send Invite", + "organizationMembership_invite_redemptionTitle": "Organization joined!", + "organizationMembership_invite_returnToDashboard": "Go to dashboard", + "system_buildFailed": "Build failed", + "system_publishFailed": "Publish failed", + "downloads_title": "Downloads" +} diff --git a/source/SIL.AppBuilder.Portal/src/lib/locales/es-419.json b/source/SIL.AppBuilder.Portal/src/lib/locales/es-419.json new file mode 100644 index 0000000000..831d4a2854 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/locales/es-419.json @@ -0,0 +1,446 @@ +{ + "appName": "Scriptoria", + "welcome": "Bienvenidos a Scriptoria", + "search": "Buscar", + "contactUs": "Contáctanos", + "exampleForm": "Formulario de ejemplo", + "updated": "¡Actualizado!", + "opensource": "Código abierto", + "home": "Inicio", + "tabAppName_zero": "Scriptoria", + "tabAppName_other": "({{count}}) Scriptoria", + "localePicker_code": "Código", + "localePicker_country": "País", + "localePicker_other": "Otros nombres", + "localePicker_label": "Lenguaje", + "localePicker_placeholder": "Buscar por idioma...", + "localePicker_region": "{num, plural,one {{Region}}other {Region}}", + "localePicker_name": "{num, plural, one {{Name}} other {Names}}", + "localePicker_tag": "{num, plural, one {{Tag}} other {Tags}}", + "localePicker_variant": "{num, plural, one {{Variant}} other {Variants}}", + "localePicker_regionName": "Nombre de región", + "attributions_title": "Proyecto de código abierto usado en Scriptoria", + "attributions_subtitle": "Librerías usadas por la licencia", + "notifications_buildComplete": "¡La construcción para {{projectName}} se ha completado!", + "notifications_buildServerConnectionLost": "¡Se ha perdido la conexión con el servidor de compilación!", + "common_search": "Buscar", + "common_noResults": "Sin resultados...", + "common_change": "Cambiar", + "common_cancel": "Cancelar", + "common_save": "Guardar", + "common_add": "Añadir", + "common_name": "Nombre", + "common_clickToEdit": "Haga clic para editar", + "common_none": "Ninguno", + "common_abbreviation": "Abreviatura", + "common_general": "Información general", + "common_notAvailable": "No disponible", + "common_archive": "Archivar", + "common_reactivate": "Reactivar", + "common_build": "Construir / Reconstruir", + "common_rebuild": "Reconstruir/Volver a publicar", + "common_updated": "Actualizado", + "common_continue": "Continuar", + "common_error": "Error", + "common_workflow": "Flujo trabajo", + "common_default": "Default", + "models_add": "Añadir {{name}}", + "models_edit": "Editar {{name}}", + "models_save": "Guardar {{name}}", + "models_createSuccess": "¡{{name}} fue creado con éxito!", + "models_updateSuccess": "¡{{name}} fue actualizado con éxito!", + "auth_title": "Scriptoria", + "auth_signup": "Registrarse", + "auth_login": "Ingresar", + "directory_title": "Directorio de proyectos ({{numProjects}})", + "directory_searchHelp": "This allows you to search over:
• Project Name
• Project Language
• Owner Name
• Organization Name
• Group Name", + "directory_filters_dateRange": "Rango de Fechas", + "header_myProfile": "Mi perfil", + "header_community": "Community", + "header_help": "Ayuda", + "header_signOut": "Salir", + "header_clearAll": "Limpiar todo", + "header_emptyNotifications": "No tienes notificaciones.", + "sidebar_myTasks_zero": "¡Mis tareas", + "sidebar_myTasks_other": "¡Mis tareas ({{count}})", + "sidebar_myProjects": "Mis Proyectos", + "sidebar_activeProjects": "Proyectos activos", + "sidebar_organizationProjects": "Proyectos de organización", + "sidebar_users": "Usuarios", + "sidebar_organizationSettings": "Configuración de organización", + "sidebar_adminSettings": "Administrador", + "sidebar_projectDirectory": "Directorio de proyectos", + "sidebar_addProject": "Agregar proyectos", + "sidebar_importProjects": "Import Projects", + "invitations_orgPrompt": "¿Te gustaría registrar a tu organización?", + "invitations_missingTokenTitle": "El token de invitación no existe", + "invitations_missingTokenPrompt": "Por favor revise el enlace y vuelva a intentarlo", + "invitations_orgInviteTitle": "¡Ud. ha sido invitado a crear una organización!", + "invitations_orgName": "Nombre de organización", + "invitations_orgUrl": "Dirección Web de la organización", + "invitations_orgSubmit": "Agregar organización", + "newOrganization_title": "Agregar organización", + "org_allOrganizations": "Todas las organizaciones", + "org_createSuccess": "¡Organización creada exitosamente!", + "org_settingsTitle": "Configuración de organización", + "org_logoUrl": "Seleccionar logo", + "org_noteLogUrl": "Nota: Para una calidad óptima, use una imagen cuadrada", + "org_productsTitle": "Productos y Publicaciones", + "org_makePrivateTitle": "Hacer Proyectos públicos por defecto", + "org_makePrivateDescription": "\n Cuando un proyecto es creado, será Público por defecto.\n (Los proyectos privados no pueden ser visto por nadie fuera de su organización)", + "org_productSelectTitle": "Selecciona todos los productos que quisieras estén disponibles para su organización", + "org_noproducts": "No hay productos disponibles", + "org_storesTitle": "Tiendas", + "org_storeSelectTitle": "Seleccione todas las tiendas que desea poner a disposición de su organización", + "org_nostores": "No hay tiendas disponibles", + "org_navBasic": "Información básica", + "org_navProducts": "Productos", + "org_navStores": "Tiendas", + "org_navGroups": "Grupos", + "org_nameError": "El nombre no puede estar vacío", + "org_abbreviationError": "La abreviatura no puede estar vacía", + "org_groupCreated": "Grupo creado", + "org_groupEdited": "Grupo editado", + "org_groupDeleted": "Grupo eliminado", + "org_navInfrastructure": "Infraestructura", + "org_infrastructureTitle": "Infraestructura", + "org_useDefaultBuildEngineTitle": "Usar el motor de compilación predeterminado", + "org_groupsTitle": "Grupos", + "org_noGroups": "Su organización no tiene grupos", + "org_addGroupButton": "Agregar grupo", + "org_basicTitle": "Información básica", + "org_orgName": "Nombre de organización", + "org_save": "Guardar", + "org_buildEngineUrl": "URL del motor de compilación", + "org_buildEngineApiAccessToken": "Token de acceso del motor de construcción", + "products_definition": "Archivos útiles que resultan de una serie de pasos.", + "products_storeSelect": "Seleccione una tienda para {{name}}", + "products_noStoresAvailable": "No hay tiendas disponibles para el producto seleccionado. Por favor, contacte con el administrador de su organización.", + "products_actions_executed": "{{actionName}} fue ejecutado con éxito.", + "products_actions_cancelled": "{{actionName}} fue cancelado con éxito.", + "products_actions_rebuild": "Reconstruir", + "products_actions_republish": "Publicar nuevamente", + "products_actions_cancel": "Cancelar", + "products_actions_noneAvailable": "No hay acciones disponibles actualmente para este producto.", + "products_actions_bulkNotAllAllowed": "No todos los productos de los proyectos seleccionados pueden tener la acción {{action}} aplicada.", + "products_actions_dispatched": "La acción {{action}} ha sido enviada.", + "products_actions_properties": "Properties", + "products_files_title": "Product Files", + "profile_title": "Perfil", + "profile_pictureTitle": "Imagen de perfil", + "profile_general": "General", + "profile_generalInformation": "Información general", + "profile_updated": "¡Perfil actualizado con éxito!", + "profile_updatePicture": "Actualize su imagen en Gravatar.com", + "profile_uploadPicture": "Cargar una nueva imagen", + "profile_firstName": "Nombre", + "profile_lastName": "Apellido", + "profile_name": "Display Name", + "profile_email": "Correo electrónico", + "profile_phone": "Teléfono", + "profile_location": "Ubicación", + "profile_timezone": "Zona horaria", + "profile_locale": "Idioma", + "profile_timezonePlaceholder": "Seleccione su zona horaria...", + "profile_notificationSettingsTitle": "Configuración de notificaciones", + "profile_optOutOfEmailOption": "Deseo recibir notificaciones por correo electrónico", + "profile_sshSettingsTitle": "Administrar claves personales SSH", + "profile_sshKeyLabel": "Publicando clave SSH", + "profile_visibleProfile": "Visibilidad del perfil", + "profile_noPhone": "Sin teléfono", + "profile_noTimezone": "(GMT-0)", + "profile_visibility_visible": "La información de mi perfil es pública", + "profile_visibility_restricted": "Mi información de perfil es restringida al público", + "errors_generic": "¡Ha ocurrido un error! {{errorMessage}}", + "errors_requiredField": "El {{field}} es requerido, pero no tiene ningún valor", + "errors_notAuthorized": "Usted debe iniciar sesión. Esto podría ser debido a que expiró un inicio de sesión previo.", + "errors_notFoundTitle": "\n¡No encontrado!", + "errors_notFoundDescription": "¡Algo salió mal y la página o recursos no se pudo encontrar!", + "errors_groupRequired": "Debe ser un miembro de al menos un grupo.", + "errors_userForbidden": "Ocurrió un error: Prohibido. Por favor, contacte al administrador de su organización.", + "errors_friendlyForbidden": "No se le permite hacer eso. Póngase en contacto con el administrador de su organización", + "errors_forbiddenForAllOrgs": "No se le permite hacer eso. Por favor, seleccione una organización", + "errors_notAMemberOfOrg": "No es un miembro de esa organización", + "errors_notactiveUser": "El usuario no está activo", + "errors_notactiveUserText": "\n Este usuario actualmente no está activo.\n
\n Póngase en contacto con el administrador de su organización para discutir el motivo del cambio de estado.\n ", + "errors_orgMembershipRequired": "Se requiere ser miembro de la organización", + "errors_orgMembershipRequiredText": "\n Para utilizar Scriptoria, debe ser miembro de por lo menos una organización.\n
\n Por favor, contacte con el administrador de su organización para solicitar una invitación a\n una organización de Scriptoria.\n ", + "errors_orgMembershipChanged": "Organization Membership has Changed", + "errors_orgMembershipChangedText": "\n Your organization membership has changed.\n
\n This can take a few moments to process. Please sign out, wait a minute, and then sign in again for the change to take effect.\n ", + "errors_orgMustBeSelected": "Debe seleccionar una organización para visualizar esta página", + "errors_verifyEmailTitle": "Por favor, verifique su dirección de correo electrónico", + "errors_verifyEmailDescription": "\nPara poder usar Scriptoria, necesitamos que verifiques tu dirección de correo electrónico.\n
\n Por favor, ingrese a su correo electrónico y haga clic en el enlace de verificación. ", + "errors_invalidProjectSelection": "Productos no válidos disponibles para el/los proyecto(s) seleccionados", + "tasks_title": "Mis tareas", + "tasks_project": "Proyecto", + "tasks_product": "Producto", + "tasks_assignedTo": "Asignado a", + "tasks_status": "Estado", + "tasks_waitTime": "Tiempo de espera", + "tasks_unclaimed": "[sin asignar]", + "tasks_noTasksTitle": "No tiene tareas asignadas.", + "tasks_noTasksDescription": "Las tareas que requieren su atención aparecerán aquí.", + "tasks_reassign": "Reasignar", + "tasks_waiting": "Esperando {{waitTime}}", + "tasks_forNames": "para {{allowedNames}} a {{activityName}}", + "tasks_scriptoria": "Scriptoria", + "projects_noBuilds": "No Builds Yet", + "projects_latestBuild": "Última compilación ({{version}})", + "projects_buildPending": "Build Pending", + "projects_buildFailed": "Build Failed", + "projects_switcher_myProjects": "Mis proyectos ({{numProjects}})", + "projects_switcher_archived": "Archivados ({{numProjects}})", + "projects_switcher_dropdown_orgProjects": "Proyectos de organización", + "projects_switcher_dropdown_myProjects": "Mis Proyectos", + "projects_switcher_dropdown_activeProjects": "Proyectos activos", + "projects_switcher_dropdown_archived": "Proyectos archivados", + "projects_switcher_dropdown_all": "Todos los proyectos", + "projects_bulk_buildModal_title": "Perform Bulk Rebuild/Republish", + "productDefinitions_filterAllProjects": "Todos los proyectos que contienen...", + "projectImport_importFile": "Import JSON File", + "projectImport_createSuccess": "Project Import successfully created. You will receive an email when the import is completed.", + "project_title": "Proyecto", + "project_organizationContact": "Contacto de la organización: ", + "project_projectOwner": "Propietario del proyecto: ", + "project_claimOwnership": "Claim Ownership", + "project_claimSuccess": "Succesfully Claimed", + "project_editProject": "Edit Project", + "project_newProject": "Nuevo proyecto", + "project_importProjects": "Import Projects", + "project_importProjectsHelp": "Import Projects Help", + "project_projectName": "Nombre de proyecto", + "project_noAvailableGroups": "No hay grupos disponibles para la organización seleccionada", + "project_projectDescription": "Descripción", + "project_projectGroup": "Grupo de proyecto", + "project_languageCode": "Código de lenguaje", + "project_type": "Tipo", + "project_public": "Público", + "project_private": "Privado", + "project_visibilityLabel": "Público", + "project_visibilityDescription": "Si usted hace su proyecto privado, no será visible en el directorio o accesible por otras organizaciones", + "project_createdOn": "Creado", + "project_overview": "Información", + "project_productFiles": "Archivos de producto", + "project_dropdown_transfer": "Transferir propiedad", + "project_dropdown_archive": "Archivar", + "project_dropdown_reactivate": "Reactivar", + "project_dropdown_build": "Compilar", + "project_details_title": "Detalles", + "project_details_language": "Lenguaje", + "project_details_type": "Tipo de proyecto", + "project_products_title": "Productos", + "project_products_empty": "No tiene productos para este proyecto.", + "project_products_addRemove": "Añadir o quitar productos", + "project_products_add": "Add Products", + "project_products_remove": "Remove Products", + "project_products_popup_addTitle": "Select Product to Add", + "project_products_popup_removeTitle": "Select Product to Remove", + "project_products_popup_done": "Hecho", + "project_products_popup_empty": "No hay productos disponibles", + "project_products_popup_details": "Detalles", + "project_products_popup_properties": "Publishing Properties", + "project_products_publications_channel": "Channel", + "project_products_publications_status": "Estado", + "project_products_publications_date": "Publish Date", + "project_products_publications_url": "Publish Url", + "project_products_publications_succeeded": "Success", + "project_products_publications_failed": "Failure", + "project_products_publications_console": "Console Text", + "project_products_options_update": "actualizar", + "project_products_options_publish": "publicar", + "project_products_transitions_productDetails": "Product Details", + "project_products_transitions_storeName": "Store Name", + "project_products_transitions_state": "State", + "project_products_transitions_user": "Usuario", + "project_products_transitions_command": "Comando", + "project_products_transitions_comment": "Comentario", + "project_products_transitions_date": "Fecha", + "project_products_transitions_transitionTypes_1": "Actividad", + "project_products_transitions_transitionTypes_2": "{{workflowType}} Workflow Started", + "project_products_transitions_transitionTypes_3": "{{workflowType}} Workflow Completed", + "project_products_transitions_transitionTypes_4": "{{workflowType}} Workflow Cancelled", + "project_products_transitions_transitionTypes_5": "{{workflowType}} Project Access", + "project_products_properties_computeType": "Compute Type", + "project_products_properties_selectComputeType": "Select compute type", + "project_products_properties_small": "Small", + "project_products_properties_medium": "Medium", + "project_products_properties_title": "Product Publishing Properties", + "project_products_updated": "Actualizado", + "project_products_size": "Tamaño", + "project_products_filename": "Nombre de archivo", + "project_products_published": "Publicado", + "project_products_unpublished": "Sin publicar", + "project_products_numArtifacts": "{amount, plural, =0 {No Product Files} other {{ amount } Product Files}}", + "project_products_noArtifacts": "No hay archivos de producto", + "project_products_rebuild": "reconstruir", + "project_products_creationInProgress": "Pendiente: No puede añadir un producto hasta que la configuración del proyecto se haya completado.", + "project_settings_title": "Configuración", + "project_settings_automaticRebuild_title": "Recompilaciones automáticas", + "project_settings_automaticRebuild_description": "Cuando las recompilaciones automáticas están activadas, Scriptoria reconstruirá sus productos automáticamente cuando el código fuente se actualice", + "project_settings_organizationDownloads_title": "Permitir descargar a otras organizaciones", + "project_settings_organizationDownloads_description": "When this setting is on, any Scriptoria User that is able to view your project in the Directory will be able to download the Product Files (the outputs of the products).", + "project_settings_visibility_title": "Visibilidad pública", + "project_settings_visibility_description": "Los proyectos públicos permiten a cualquier usuario de Scriptoria ver su proyecto en el Directorio", + "project_side_repositoryLocation": "Ubicación del repositorio", + "project_side_organization": "Organización", + "project_side_projectOwner": "Propietario del proyecto", + "project_side_projectGroup": "Grupo de proyecto", + "project_side_authors_title": "Authors", + "project_side_authors_add": "add author", + "project_side_authors_close": "cerrar", + "project_side_authors_form_user": "user", + "project_side_authors_form_userError": "User cannot be empty", + "project_side_authors_form_submit": "Add Author", + "project_side_reviewers_title": "Revisores", + "project_side_reviewers_add": "Agregar revisor", + "project_side_reviewers_close": "cerrar", + "project_side_reviewers_form_name": "nombre", + "project_side_reviewers_form_nameError": "Nombre no puede estar vacío", + "project_side_reviewers_form_email": "correo electrónico", + "project_side_reviewers_form_emptyEmailError": "Correo electrónico no puede estar vacío", + "project_side_reviewers_form_invalidEmailError": "Dirección de correo electrónico no válida", + "project_side_reviewers_form_submit": "Agregar revisor", + "project_operations_archive_success": "Proyecto archivado", + "project_operations_archive_error": "Ocurrió un error al intentar archivar el proyecto", + "project_operations_reactivate_success": "Proyecto reactivado", + "project_operations_reactivate_error": "Ocurrió un error al intentar reactivar el proyecto", + "project_operations_automaticBuilds_on": "Compilaciones automáticas está activada", + "project_operations_automaticBuilds_off": "Compilaciones automáticas está desactivada", + "project_operations_automaticBuilds_error": "Error: No pudimos cambiar el estado de \"compilación automática\"", + "project_operations_allowDownloads_on": "Permitir descarga de proyecto está activada", + "project_operations_allowDownloads_off": "Permitir descarga de proyecto está desactivada", + "project_operations_allowDownloads_error": "Error: No pudimos cambiar el estado de \"permitir descargas\"", + "project_operations_isPublic_on": "La visibilidad del proyecto está establecida como Público", + "project_operations_isPublic_off": "La visibilidad del proyecto está establecida como Privado", + "project_operations_isPublic_error": "Error: No se ha podido cambiar la visibilidad del proyecto", + "projectTable_columns_project": "Proyecto", + "projectTable_columns_owner": "Propietario", + "projectTable_columns_organization": "Organización", + "projectTable_columns_language": "Idioma", + "projectTable_columns_group": "Grupo", + "projectTable_columns_buildVersion": "Versión de compilación", + "projectTable_columns_buildDate": "Fecha de compilación", + "projectTable_columns_createdOn": "Fecha de creación", + "projectTable_columns_updatedOn": "Fecha de actualización", + "projectTable_columns_activeSince": "Activo desde", + "projectTable_empty": "No se encontraron proyectos", + "projectTable_products": "Productos", + "projectTable_noProducts": "Este proyecto no tiene productos configurados", + "users_settingsTitle": "Configuración de usuario", + "users_userProfile": "Perfil de usuario", + "users_userGroups": "Membresías de grupo", + "users_userRoles": "Roles de la organización", + "users_noneFound": "No se encontraron usuarios con esos criterios de búsqueda.", + "users_noRoles": "No hay roles asignados", + "users_title": "Administrar usuarios", + "users_addUser_placeholder": "Ingrese un correo electrónico", + "users_addUser_button": "Añadir usuario a {{organization}}", + "users_addUser_modalTitle": "Añadir usuario a {{organization}}", + "users_addUser_modalAddButton": "Añadir", + "users_addUser_error": "No se ha encontrado ningún usuario con esa dirección de correo electrónico. Por favor, pídeles que cree una cuenta.", + "users_table_columns_name": "Nombre", + "users_table_columns_role": "Rol", + "users_table_columns_groups": "Grupos", + "users_table_columns_active": "Activo", + "users_operations_lock_success": "Usuario bloqueado. Un usuario bloqueado no podrá iniciar sesión en Scriptoria.", + "users_operations_lock_error": "Ocurrió un error al bloquear el usuario", + "users_operations_unlock_success": "Usuario activo. El usuario ahora podrá iniciar sesión en Scriptoria.", + "users_operations_unlock_error": "ocurrió un error al activar el usuario", + "stores_name": "Tienda", + "stores_listTitle": "Tiendas", + "stores_attributes_name": "Nombre", + "stores_attributes_description": "Descripción", + "storeTypes_name": "Tipo de tienda", + "storeTypes_listTitle": "Tipos de tienda", + "admin_settings_title": "Configuración del administrador", + "admin_settings_navigation_organizations": "Organizaciones", + "admin_settings_navigation_workflowdefinitions": "Definiciones de flujo de trabajo", + "admin_settings_navigation_productDefinitions": "Definiciones de producto", + "admin_settings_navigation_storeTypes": "Tipos de tienda", + "admin_settings_navigation_stores": "Tiendas", + "admin_settings_navigation_buildEngines": "Build Engine Statuses", + "admin_settings_organizations_title": "Organizaciones", + "admin_settings_organizations_name": "Nombre de la organización", + "admin_settings_organizations_owner": "Propietario", + "admin_settings_organizations_emptyOwner": "Necesita seleccionar un propietario para esta organización", + "admin_settings_organizations_add": "Añadir organización", + "admin_settings_organizations_addSuccess": "Organización añadida", + "admin_settings_organizations_edit": "Editar organización", + "admin_settings_organizations_editSuccess": "Organización actualizada", + "admin_settings_organizations_websiteURL": "URL del sitio web", + "admin_settings_organizations_buildEngineURL": "Build Engine URL", + "admin_settings_organizations_accessToken": "Build Engine API Access Token", + "admin_settings_organizations_logoURL": "URL del logotipo", + "admin_settings_organizations_publicByDefault": "Público por defecto", + "admin_settings_organizations_publicByDefaultDescription": "Los proyectos creados bajo esta organización son públicos por defecto", + "admin_settings_workflowDefinitions_title": "Definiciones de flujo de trabajo", + "admin_settings_workflowDefinitions_name": "Nombre de la definición del flujo de trabajo", + "admin_settings_workflowDefinitions_description": "Descripción", + "admin_settings_workflowDefinitions_workflowScheme": "Workflow Scheme", + "admin_settings_workflowDefinitions_workflowBusinessFlow": "Workflow Business Flow", + "admin_settings_workflowDefinitions_properties": "Propiedades", + "admin_settings_workflowDefinitions_add": "Add Workflow Definition", + "admin_settings_workflowDefinitions_edit": "Edit Workflow Definition", + "admin_settings_workflowDefinitions_storeType": "Tipo de tienda", + "admin_settings_workflowDefinitions_enabled": "Habilitado", + "admin_settings_workflowDefinitions_enabledDescription": "Workflow definitions may not be used until enabled.", + "admin_settings_workflowDefinitions_workflowAdded": "Workflow Definition added", + "admin_settings_workflowDefinitions_workflowUpdated": "Workflow Definition updated", + "admin_settings_workflowDefinitions_emptyStoreType": "Need to select a store type for this workflow definition", + "admin_settings_workflowDefinitions_emptyWorkflowType": "Need to select a workflow type for this workflow definition", + "admin_settings_workflowDefinitions_workflowType": "Workflow Type", + "admin_settings_workflowDefinitions_workflowTypes_1": "Startup", + "admin_settings_workflowDefinitions_workflowTypes_2": "Reconstruir", + "admin_settings_workflowDefinitions_workflowTypes_3": "Publicar nuevamente", + "admin_settings_productDefinitions_title": "Definiciones de producto", + "admin_settings_productDefinitions_name": "Product definition Name", + "admin_settings_productDefinitions_add": "Añadir definición de producto", + "admin_settings_productDefinitions_edit": "Editar definición de producto", + "admin_settings_productDefinitions_description": "Descripción", + "admin_settings_productDefinitions_properties": "Propiedades", + "admin_settings_productDefinitions_type": "Tipo de aplicación", + "admin_settings_productDefinitions_workflow": "Flujo trabajo", + "admin_settings_productDefinitions_republishWorkflow": "Republish Workflow", + "admin_settings_productDefinitions_rebuildWorkflow": "Rebuild Workflow", + "admin_settings_productDefinitions_addSuccess": "Definición del producto añadida", + "admin_settings_productDefinitions_editSuccess": "Definición del producto actualizada", + "admin_settings_productDefinitions_emptyName": "Nombre no puede estar vacío", + "admin_settings_productDefinitions_emptyType": "Necesita seleccionar un tipo de aplicación", + "admin_settings_productDefinitions_emptyWorkflow": "Need to select a Workflow definition", + "admin_settings_productDefinitions_noWorkflow": "No Workflow Required", + "admin_settings_storeTypes_title": "Tipos de tienda", + "admin_settings_storeTypes_name": "Store Type Name", + "admin_settings_storeTypes_add": "Añadir tipo de tienda", + "admin_settings_storeTypes_edit": "Editar tipo de tienda", + "admin_settings_storeTypes_description": "Descripción", + "admin_settings_storeTypes_type": "Tipo de aplicación", + "admin_settings_storeTypes_workflow": "Flujo de trabajo", + "admin_settings_storeTypes_addSuccess": "Tipo de tienda añadido", + "admin_settings_storeTypes_editSuccess": "Tipo de tienda actualizado", + "admin_settings_storeTypes_emptyName": "Nombre no puede estar vacío", + "admin_settings_storeTypes_emptyType": "Necesita seleccionar un tipo de aplicación", + "admin_settings_storeTypes_emptyWorkflow": "Need to select a Workflow definition", + "admin_settings_buildEngines_title": "Build Engine Statuses", + "admin_settings_buildEngines_accessToken": "Token de acceso del motor de construcción", + "admin_settings_buildEngines_websiteURL": "URL del sitio web", + "admin_settings_buildEngines_connected": "Connected", + "admin_settings_buildEngines_disconnected": "Disconnected", + "admin_settings_buildEngines_status": "Estado", + "admin_settings_buildEngines_lastUpdated": "Last update", + "organizationMembership_invite_error_expired": "La invitación ha caducado", + "organizationMembership_invite_error_redeemed": "La invitación ya ha sido canjeada", + "organizationMembership_invite_error_notFound": "No se encontró la invitación", + "organizationMembership_invite_error_unexpected": "Ocurrió un error inesperado: {{response}}", + "organizationMembership_invite_error_invalidResponse": "Hemos recibido una respuesta no válida... por favor, actualiza la página para intentarlo de nuevo.", + "organizationMembership_invite_create_success": "Invitación enviada a {{email}}.", + "organizationMembership_invite_create_error": "Ocurrió un error mientras se intentaba invitar al usuario.", + "organizationMembership_invite_create_inviteUserButtonTitle": "Invitar usuario", + "organizationMembership_invite_create_inviteUserModalTitle": "Invitar usuario", + "organizationMembership_invite_create_emailInputPlaceholder": "Introduzca dirección de correo electrónico", + "organizationMembership_invite_create_sendInviteButton": "Enviar invitación", + "organizationMembership_invite_redemptionTitle": "¡Se unió a la organización!", + "organizationMembership_invite_returnToDashboard": "Go to dashboard", + "system_buildFailed": "Build failed", + "system_publishFailed": "Publish failed", + "downloads_title": "Downloads" +} diff --git a/source/SIL.AppBuilder.Portal/src/lib/locales/fr-FR.json b/source/SIL.AppBuilder.Portal/src/lib/locales/fr-FR.json new file mode 100644 index 0000000000..2f9f08d734 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/locales/fr-FR.json @@ -0,0 +1,447 @@ +{ + "appName": "Scriptoria", + "welcome": "Bienvenue sur Scriptoria", + "search": "Search", + "contactUs": "Contact Us", + "exampleForm": "Example Form", + "updated": "Updated!", + "opensource": "Open Source", + "home": "Home", + "tabAppName_zero": "Scriptoria", + "tabAppName_other": "({{count}}) Scriptoria", + "localePicker_code": "Code", + "localePicker_country": "Country", + "localePicker_other": "Other Names", + "localePicker_label": "Language", + "localePicker_placeholder": "Search by Language...", + "localePicker_region": "{num, plural, one {{Region}} other {Regions}}", + "localePicker_name": "{num, plural, one {{Name}} other {Names}}", + "localePicker_tag": "{num, plural, one {{Tag}} other {Tags}}", + "localePicker_variant": "{num, plural, one {{Variant}} other {Variants}}", + "localePicker_regionName": "Region Name", + "attributions_title": "Open Source Project used in Scriptoria", + "attributions_subtitle": "Libraries used by License", + "notifications_buildComplete": "The build for {{projectName}} has completed!", + "notifications_buildServerConnectionLost": "Connection to the build server has been lost!", + "common_search": "Search", + "common_noResults": "No Results...", + "common_change": "Change", + "common_cancel": "Cancel", + "common_save": "Save", + "common_add": "Add", + "common_name": "Name", + "common_clickToEdit": "Click to edit", + "common_none": "None", + "common_abbreviation": "Abbreviation", + "common_general": "General Information", + "common_notAvailable": "Not Available", + "common_archive": "Archive", + "common_reactivate": "Reactivate", + "common_build": "Build / Rebuild", + "common_rebuild": "Rebuild / Republish", + "common_updated": "Updated", + "common_continue": "Continue", + "common_error": "Error", + "common_workflow": "Workflow", + "common_default": "Default", + "models_add": "Add {{name}}", + "models_edit": "Edit {{name}}", + "models_save": "Save {{name}}", + "models_createSuccess": "{{name}} was successfully created!", + "models_updateSuccess": "{{name}} was successfully updated!", + "auth_title": "Scriptoria", + "auth_signup": "Sign Up", + "auth_login": "Log In", + "directory_title_zero": "Project Directory", + "directory_title_other": "Project Directory ({{numProjects}})", + "directory_searchHelp": "This allows you to search over:
• Project Name
• Project Language
• Owner Name
• Organization Name
• Group Name", + "directory_filters_dateRange": "Project Updated Date Between", + "header_myProfile": "My Profile", + "header_community": "Community", + "header_help": "Help", + "header_signOut": "Sign out", + "header_clearAll": "Clear All", + "header_emptyNotifications": "You have no notifications.", + "sidebar_myTasks_zero": "My Tasks", + "sidebar_myTasks_other": "My Tasks ({{count}})", + "sidebar_myProjects": "My Projects", + "sidebar_activeProjects": "Active Projects", + "sidebar_organizationProjects": "Organization Projects", + "sidebar_users": "Users", + "sidebar_organizationSettings": "Organization Settings", + "sidebar_adminSettings": "Admin", + "sidebar_projectDirectory": "Project Directory", + "sidebar_addProject": "Add Project", + "sidebar_importProjects": "Import Projects", + "invitations_orgPrompt": "Like to sign up your organization?", + "invitations_missingTokenTitle": "Your invitation token is missing", + "invitations_missingTokenPrompt": "Please check the link and try again", + "invitations_orgInviteTitle": "You have been invited to create an organization!", + "invitations_orgName": "Organization Name", + "invitations_orgUrl": "Organization Website URL", + "invitations_orgSubmit": "Add Organization", + "newOrganization_title": "Add organization", + "org_allOrganizations": "All Organizations", + "org_createSuccess": "Organization created successfully!", + "org_settingsTitle": "Organization Settings", + "org_logoUrl": "Logo URL", + "org_noteLogUrl": "Note: For optimal quality use a square image", + "org_productsTitle": "Products and Publishing", + "org_makePrivateTitle": "Make Projects Public by Default", + "org_makePrivateDescription": "\n When a new project is created, it will be defaulted to Public.\n (Private projects cannot be viewed by anyone outside of your organization)", + "org_productSelectTitle": "Select all the products you would like to make available to your organization", + "org_noproducts": "No products available", + "org_storesTitle": "Stores", + "org_storeSelectTitle": "Select all the stores you would like to make available to your organization", + "org_nostores": "No stores available", + "org_navBasic": "Basic Info", + "org_navProducts": "Products & Publishing", + "org_navStores": "Stores", + "org_navGroups": "Groups", + "org_nameError": "Name cannot be empty", + "org_abbreviationError": "Abbreviation cannot be empty", + "org_groupCreated": "Group created", + "org_groupEdited": "Group edited", + "org_groupDeleted": "Group deleted", + "org_navInfrastructure": "Infrastructure", + "org_infrastructureTitle": "Infrastructure", + "org_useDefaultBuildEngineTitle": "Use Default Build Engine", + "org_groupsTitle": "Groups", + "org_noGroups": "Your organization has no groups", + "org_addGroupButton": "Add Group", + "org_basicTitle": "Basic Info", + "org_orgName": "Organization Name", + "org_save": "Save", + "org_buildEngineUrl": "Build Engine URL", + "org_buildEngineApiAccessToken": "Build Engine API Access Token", + "products_definition": "Useful files that result from a series of steps.", + "products_storeSelect": "Select a store for {{name}}", + "products_noStoresAvailable": "There are no stores available for the selected product. Please contact your organization administrator.", + "products_actions_executed": "{{actionName}} was successfully executed.", + "products_actions_cancelled": "{{actionName}} was successfully cancelled.", + "products_actions_rebuild": "Rebuild", + "products_actions_republish": "Republish", + "products_actions_cancel": "Cancel", + "products_actions_noneAvailable": "No actions are currently available for this product.", + "products_actions_bulkNotAllAllowed": "Not all of the selected projects' products can have the {{action}} action(s) applied.", + "products_actions_dispatched": "The {{action}} action has been dispatched.", + "products_actions_properties": "Properties", + "products_files_title": "Product Files", + "profile_title": "Profile", + "profile_pictureTitle": "Profile Picture", + "profile_general": "General", + "profile_generalInformation": "General Information", + "profile_updated": "Profile updated successfully!", + "profile_updatePicture": "Update your picture at Gravatar.com", + "profile_uploadPicture": "Upload new picture", + "profile_firstName": "First name", + "profile_lastName": "Last name", + "profile_name": "Display Name", + "profile_email": "Email", + "profile_phone": "Phone", + "profile_location": "Location", + "profile_timezone": "Timezone", + "profile_locale": "Locale", + "profile_timezonePlaceholder": "Select your timezone...", + "profile_notificationSettingsTitle": "Notification Settings", + "profile_optOutOfEmailOption": "I want to receive email notifications", + "profile_sshSettingsTitle": "Manage Personal SSH Keys", + "profile_sshKeyLabel": "Publishing SSH Key", + "profile_visibleProfile": "Profile visibility", + "profile_noPhone": "no phone added", + "profile_noTimezone": "(GMT-0)", + "profile_visibility_visible": "My profile information is publicly viewable", + "profile_visibility_restricted": "My profile information is restricted from the public", + "errors_generic": "An error occurred! {{errorMessage}}", + "errors_requiredField": "The {{field}} is required, but has no value", + "errors_notAuthorized": "You must login. This could be due to a prior login expiring.", + "errors_notFoundTitle": "Not Found!", + "errors_notFoundDescription": "Something went wrong and the page or resource could not be found!", + "errors_groupRequired": "You must be a member of at least one group.", + "errors_userForbidden": "An error occurred: Forbidden. Please contact your organization administrator.", + "errors_friendlyForbidden": "You are not allowed to do that. Please contact your organization administrator", + "errors_forbiddenForAllOrgs": "You are not allowed to do that. Please select an organization", + "errors_notAMemberOfOrg": "You are not a member of that organization", + "errors_notactiveUser": "User is not Active", + "errors_notactiveUserText": "\n This user is currently set to not active.\n
\n Please contact your organization administrator to discuss the reason for the status change.\n ", + "errors_orgMembershipRequired": "Organization Membership is Required", + "errors_orgMembershipRequiredText": "\n In order to use Scriptoria, you must be a member of at least one organization.\n
\n Please contact your organization administrator to discuss receiving an invite to\n an organization on Scriptoria.\n ", + "errors_orgMembershipChanged": "Organization Membership has Changed", + "errors_orgMembershipChangedText": "\n Your organization membership has changed.\n
\n This can take a few moments to process. Please sign out, wait a minute, and then sign in again for the change to take effect.\n ", + "errors_orgMustBeSelected": "An organization must be selected to view this page", + "errors_verifyEmailTitle": "Please verify your email address", + "errors_verifyEmailDescription": "\n In order to use Scriptoria, we need you to verify your email address.\n
\n Please log into your email and clik the verification link. ", + "errors_invalidProjectSelection": "No valid products available for selected project(s)", + "tasks_title": "My Tasks", + "tasks_project": "Project", + "tasks_product": "Product", + "tasks_assignedTo": "Assigned To", + "tasks_status": "Status", + "tasks_waitTime": "Wait Time", + "tasks_unclaimed": "[unclaimed]", + "tasks_noTasksTitle": "No tasks are assigned to you.", + "tasks_noTasksDescription": "Tasks that require your attention will appear here.", + "tasks_reassign": "Reassign", + "tasks_waiting": "Waiting {{waitTime}}", + "tasks_forNames": "for {{allowedNames}} to {{activityName}}", + "tasks_scriptoria": "Scriptoria", + "projects_noBuilds": "No Builds Yet", + "projects_latestBuild": "Latest Build ({{version}})", + "projects_buildPending": "Build Pending", + "projects_buildFailed": "Build Failed", + "projects_switcher_myProjects": "My Projects ({{numProjects}})", + "projects_switcher_archived": "Archived ({{numProjects}})", + "projects_switcher_dropdown_orgProjects": "Organization Projects", + "projects_switcher_dropdown_myProjects": "My Projects", + "projects_switcher_dropdown_activeProjects": "Active Projects", + "projects_switcher_dropdown_archived": "Archived Projects", + "projects_switcher_dropdown_all": "All Projects", + "projects_bulk_buildModal_title": "Perform Bulk Rebuild/Republish", + "productDefinitions_filterAllProjects": "All projects that contain...", + "projectImport_importFile": "Import JSON File", + "projectImport_createSuccess": "Project Import successfully created. You will receive an email when the import is completed.", + "project_title": "Project", + "project_organizationContact": "Organization Contact: ", + "project_projectOwner": "Project Owner: ", + "project_claimOwnership": "Claim Ownership", + "project_claimSuccess": "Succesfully Claimed", + "project_editProject": "Edit Project", + "project_newProject": "New Project", + "project_importProjects": "Import Projects", + "project_importProjectsHelp": "Import Projects Help", + "project_projectName": "Project Name", + "project_noAvailableGroups": "There are no available groups for the selected organization", + "project_projectDescription": "Description", + "project_projectGroup": "Project Group", + "project_languageCode": "Language Code", + "project_type": "Type", + "project_public": "Public", + "project_private": "Private", + "project_visibilityLabel": "Public", + "project_visibilityDescription": "If you make your project Private, it will not be visible in the directory or accessible by other organizations", + "project_createdOn": "Created", + "project_overview": "Overview", + "project_productFiles": "Product files", + "project_dropdown_transfer": "Transfer Ownership", + "project_dropdown_archive": "Archive", + "project_dropdown_reactivate": "Reactivate", + "project_dropdown_build": "Build", + "project_details_title": "Details", + "project_details_language": "Language", + "project_details_type": "Project Type", + "project_products_title": "Products", + "project_products_empty": "You have no products for this project.", + "project_products_addRemove": "Add or Remove Products", + "project_products_add": "Add Products", + "project_products_remove": "Remove Products", + "project_products_popup_addTitle": "Select Product to Add", + "project_products_popup_removeTitle": "Select Product to Remove", + "project_products_popup_done": "Done", + "project_products_popup_empty": "No products available", + "project_products_popup_details": "Details", + "project_products_popup_properties": "Publishing Properties", + "project_products_publications_channel": "Channel", + "project_products_publications_status": "Status", + "project_products_publications_date": "Publish Date", + "project_products_publications_url": "Publish Url", + "project_products_publications_succeeded": "Success", + "project_products_publications_failed": "Failure", + "project_products_publications_console": "Console Text", + "project_products_options_update": "update", + "project_products_options_publish": "publish", + "project_products_transitions_productDetails": "Product Details", + "project_products_transitions_storeName": "Store Name", + "project_products_transitions_state": "State", + "project_products_transitions_user": "User", + "project_products_transitions_command": "Command", + "project_products_transitions_comment": "Comment", + "project_products_transitions_date": "Date", + "project_products_transitions_transitionTypes_1": "Activity", + "project_products_transitions_transitionTypes_2": "{{workflowType}} Workflow Started", + "project_products_transitions_transitionTypes_3": "{{workflowType}} Workflow Completed", + "project_products_transitions_transitionTypes_4": "{{workflowType}} Workflow Cancelled", + "project_products_transitions_transitionTypes_5": "{{workflowType}} Project Access", + "project_products_properties_computeType": "Compute Type", + "project_products_properties_selectComputeType": "Select compute type", + "project_products_properties_small": "Small", + "project_products_properties_medium": "Medium", + "project_products_properties_title": "Product Publishing Properties", + "project_products_updated": "Updated", + "project_products_size": "Size", + "project_products_filename": "Filename", + "project_products_published": "Published", + "project_products_unpublished": "Unpublished", + "project_products_numArtifacts": "{amount, plural, =0 {No Product Files} other {{ amount } Product Files}}", + "project_products_noArtifacts": "No Product Files", + "project_products_rebuild": "rebuild", + "project_products_creationInProgress": "Pending: You cannot add a product until the project setup has been completed.", + "project_settings_title": "Settings", + "project_settings_automaticRebuild_title": "Automatic Rebuilds", + "project_settings_automaticRebuild_description": "When automatic rebuilds are on, Scriptoria will automatically rebuild your products when the input source is updated", + "project_settings_organizationDownloads_title": "Allow Other Organizations to download", + "project_settings_organizationDownloads_description": "When this setting is on, any Scriptoria User that is able to view your project in the Directory will be able to download the Product Files (the outputs of the products).", + "project_settings_visibility_title": "Public Visibility", + "project_settings_visibility_description": "Public Projects allow any Scriptoria User to view your project in the Directory", + "project_side_repositoryLocation": "Repository Location", + "project_side_organization": "Organization", + "project_side_projectOwner": "Project Owner", + "project_side_projectGroup": "Project Group", + "project_side_authors_title": "Authors", + "project_side_authors_add": "add author", + "project_side_authors_close": "close", + "project_side_authors_form_user": "user", + "project_side_authors_form_userError": "User cannot be empty", + "project_side_authors_form_submit": "Add Author", + "project_side_reviewers_title": "Reviewers", + "project_side_reviewers_add": "add reviewer", + "project_side_reviewers_close": "close", + "project_side_reviewers_form_name": "name", + "project_side_reviewers_form_nameError": "Name cannot be empty", + "project_side_reviewers_form_email": "email", + "project_side_reviewers_form_emptyEmailError": "Email cannot be empty", + "project_side_reviewers_form_invalidEmailError": "Invalid email address", + "project_side_reviewers_form_submit": "Add Reviewer", + "project_operations_archive_success": "Project archived", + "project_operations_archive_error": "There was an error trying to archive the project", + "project_operations_reactivate_success": "Project reactivated", + "project_operations_reactivate_error": "There was an error trying to reactivate the project", + "project_operations_automaticBuilds_on": "Automatic builds is ON", + "project_operations_automaticBuilds_off": "Automatic builds is OFF", + "project_operations_automaticBuilds_error": "Error: We could not change automatic build status", + "project_operations_allowDownloads_on": "Allow project download is ON", + "project_operations_allowDownloads_off": "Allow project download is OFF", + "project_operations_allowDownloads_error": "Error: We could not change Allow Download status", + "project_operations_isPublic_on": "Project visibility is set to Public", + "project_operations_isPublic_off": "Project visibility is set to Private", + "project_operations_isPublic_error": "Error: We could not change Project visibility", + "projectTable_columns_project": "Project", + "projectTable_columns_owner": "Owner", + "projectTable_columns_organization": "Organization", + "projectTable_columns_language": "Language", + "projectTable_columns_group": "Group", + "projectTable_columns_buildVersion": "Build Version", + "projectTable_columns_buildDate": "Build Date", + "projectTable_columns_createdOn": "Created On", + "projectTable_columns_updatedOn": "Updated On", + "projectTable_columns_activeSince": "Active Since", + "projectTable_empty": "No projects found", + "projectTable_products": "Products", + "projectTable_noProducts": "This project has no configured products", + "users_settingsTitle": "User Settings", + "users_userProfile": "User Profile", + "users_userGroups": "Group Memberships", + "users_userRoles": "Organization Roles", + "users_noneFound": "No users matching the selected criteria were found.", + "users_noRoles": "No roles assigned", + "users_title": "Manage Users", + "users_addUser_placeholder": "Enter an email", + "users_addUser_button": "Add User to {{organization}}", + "users_addUser_modalTitle": "Add User to {{organization}}", + "users_addUser_modalAddButton": "Add", + "users_addUser_error": "No user was found with that email address. Please ask them to create an account.", + "users_table_columns_name": "Name", + "users_table_columns_role": "Role", + "users_table_columns_groups": "Groups", + "users_table_columns_active": "Active", + "users_operations_lock_success": "User is locked. A locked user will not be able to log in to Scriptoria.", + "users_operations_lock_error": "There was an error locking the user", + "users_operations_unlock_success": "User is active. The user will now be able to log in to Scriptoria.", + "users_operations_unlock_error": "there was an error activating the user", + "stores_name": "Store", + "stores_listTitle": "Stores", + "stores_attributes_name": "Name", + "stores_attributes_description": "Description", + "storeTypes_name": "Store Type", + "storeTypes_listTitle": "Store Types", + "admin_settings_title": "Admin settings", + "admin_settings_navigation_organizations": "Organizations", + "admin_settings_navigation_workflowdefinitions": "Workflow Definitions", + "admin_settings_navigation_productDefinitions": "Product Definitions", + "admin_settings_navigation_storeTypes": "Store Types", + "admin_settings_navigation_stores": "Stores", + "admin_settings_navigation_buildEngines": "Build Engine Statuses", + "admin_settings_organizations_title": "Organizations", + "admin_settings_organizations_name": "Organization Name", + "admin_settings_organizations_owner": "Owner", + "admin_settings_organizations_emptyOwner": "Need to select an owner for this organization", + "admin_settings_organizations_add": "Add Organization", + "admin_settings_organizations_addSuccess": "Organization added", + "admin_settings_organizations_edit": "Edit Organization", + "admin_settings_organizations_editSuccess": "Organization updated", + "admin_settings_organizations_websiteURL": "Website URL", + "admin_settings_organizations_buildEngineURL": "Build Engine URL", + "admin_settings_organizations_accessToken": "Build Engine API Access Token", + "admin_settings_organizations_logoURL": "Logo URL", + "admin_settings_organizations_publicByDefault": "Public By Default", + "admin_settings_organizations_publicByDefaultDescription": "Projects created under this organization are set to public by default", + "admin_settings_workflowDefinitions_title": "Workflow Definitions", + "admin_settings_workflowDefinitions_name": "Workflow Definition Name", + "admin_settings_workflowDefinitions_description": "Description", + "admin_settings_workflowDefinitions_workflowScheme": "Workflow Scheme", + "admin_settings_workflowDefinitions_workflowBusinessFlow": "Workflow Business Flow", + "admin_settings_workflowDefinitions_properties": "Properties", + "admin_settings_workflowDefinitions_add": "Add Workflow Definition", + "admin_settings_workflowDefinitions_edit": "Edit Workflow Definition", + "admin_settings_workflowDefinitions_storeType": "Store Type", + "admin_settings_workflowDefinitions_enabled": "Enabled", + "admin_settings_workflowDefinitions_enabledDescription": "Workflow definitions may not be used until enabled.", + "admin_settings_workflowDefinitions_workflowAdded": "Workflow Definition added", + "admin_settings_workflowDefinitions_workflowUpdated": "Workflow Definition updated", + "admin_settings_workflowDefinitions_emptyStoreType": "Need to select a store type for this workflow definition", + "admin_settings_workflowDefinitions_emptyWorkflowType": "Need to select a workflow type for this workflow definition", + "admin_settings_workflowDefinitions_workflowType": "Workflow Type", + "admin_settings_workflowDefinitions_workflowTypes_1": "Startup", + "admin_settings_workflowDefinitions_workflowTypes_2": "Rebuild", + "admin_settings_workflowDefinitions_workflowTypes_3": "Republish", + "admin_settings_productDefinitions_title": "Product Definitions", + "admin_settings_productDefinitions_name": "Product definition Name", + "admin_settings_productDefinitions_add": "Add Product Definition", + "admin_settings_productDefinitions_edit": "Edit Product Definition", + "admin_settings_productDefinitions_description": "Description", + "admin_settings_productDefinitions_properties": "Properties", + "admin_settings_productDefinitions_type": "Application Type", + "admin_settings_productDefinitions_workflow": "Workflow", + "admin_settings_productDefinitions_republishWorkflow": "Republish Workflow", + "admin_settings_productDefinitions_rebuildWorkflow": "Rebuild Workflow", + "admin_settings_productDefinitions_addSuccess": "Product Definition added", + "admin_settings_productDefinitions_editSuccess": "Product Definition updated", + "admin_settings_productDefinitions_emptyName": "Name cannot be empty", + "admin_settings_productDefinitions_emptyType": "Need to select an Application Type", + "admin_settings_productDefinitions_emptyWorkflow": "Need to select a Workflow definition", + "admin_settings_productDefinitions_noWorkflow": "No Workflow Required", + "admin_settings_storeTypes_title": "Store Types", + "admin_settings_storeTypes_name": "Store Type Name", + "admin_settings_storeTypes_add": "Add Store Type", + "admin_settings_storeTypes_edit": "Edit Store Type", + "admin_settings_storeTypes_description": "Description", + "admin_settings_storeTypes_type": "Application Type", + "admin_settings_storeTypes_workflow": "Workflow", + "admin_settings_storeTypes_addSuccess": "Store Type added", + "admin_settings_storeTypes_editSuccess": "Store Type updated", + "admin_settings_storeTypes_emptyName": "Name cannot be empty", + "admin_settings_storeTypes_emptyType": "Need to select an Application Type", + "admin_settings_storeTypes_emptyWorkflow": "Need to select a Workflow definition", + "admin_settings_buildEngines_title": "Build Engine Statuses", + "admin_settings_buildEngines_accessToken": "Build Engine API Access Token", + "admin_settings_buildEngines_websiteURL": "Website URL", + "admin_settings_buildEngines_connected": "Connected", + "admin_settings_buildEngines_disconnected": "Disconnected", + "admin_settings_buildEngines_status": "Status", + "admin_settings_buildEngines_lastUpdated": "Last update", + "organizationMembership_invite_error_expired": "Invitation has expired", + "organizationMembership_invite_error_redeemed": "Invitation has already been redeemed", + "organizationMembership_invite_error_notFound": "Invitation was not found", + "organizationMembership_invite_error_unexpected": "Unexpected error occured: {{response}}", + "organizationMembership_invite_error_invalidResponse": "We've received an invalid response... please refresh the page to try again.", + "organizationMembership_invite_create_success": "Invite sent to {{email}}.", + "organizationMembership_invite_create_error": "Error occured while trying to invite user.", + "organizationMembership_invite_create_inviteUserButtonTitle": "Invite User", + "organizationMembership_invite_create_inviteUserModalTitle": "Invite User", + "organizationMembership_invite_create_emailInputPlaceholder": "Enter email address", + "organizationMembership_invite_create_sendInviteButton": "Send Invite", + "organizationMembership_invite_redemptionTitle": "Organization joined!", + "organizationMembership_invite_returnToDashboard": "Go to dashboard", + "system_buildFailed": "Build failed", + "system_publishFailed": "Publish failed", + "downloads_title": "Downloads" +} diff --git a/source/SIL.AppBuilder.Portal/src/lib/organizationInvites.ts b/source/SIL.AppBuilder.Portal/src/lib/organizationInvites.ts new file mode 100644 index 0000000000..afb2ef1324 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/organizationInvites.ts @@ -0,0 +1,38 @@ +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; + +export async function checkInviteErrors(inviteToken?: string | null) { + try { + const invite = await prisma.organizationMembershipInvites.findFirst({ + where: { + Token: inviteToken + '' + } + }); + if (!invite || !inviteToken) return { error: 'not found' }; + if (invite.Redeemed) return { error: 'redeemed' }; + if (!invite.Expires || invite.Expires < new Date()) return { error: 'expired' }; + return {}; + } catch (e) { + return { error: 'not found' }; + } +} +export async function acceptOrganizationInvite(userId: number, inviteToken: string) { + const invite = await prisma.organizationMembershipInvites.findFirst({ + where: { + Token: inviteToken + }, + include: { + Organization: true + } + }); + // Redundant check for invite validity in case checkInviteErrors was not called + if (!invite || invite.Redeemed || !invite.Expires || invite.Expires < new Date()) + return { error: 'failed' }; + if (await DatabaseWrites.organizationMemberships.acceptOrganizationInvite(userId, inviteToken)) + return { + joinedOrganization: { + logoUrl: invite.Organization.LogoUrl, + name: invite.Organization.Name + } + }; + return { error: 'failed' }; +} diff --git a/source/SIL.AppBuilder.Portal/src/lib/products/components/ProductDetails.svelte b/source/SIL.AppBuilder.Portal/src/lib/products/components/ProductDetails.svelte new file mode 100644 index 0000000000..1f0c151dce --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/products/components/ProductDetails.svelte @@ -0,0 +1,149 @@ + + + + + + + + diff --git a/source/SIL.AppBuilder.Portal/src/lib/products/index.ts b/source/SIL.AppBuilder.Portal/src/lib/products/index.ts new file mode 100644 index 0000000000..a2652e153e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/products/index.ts @@ -0,0 +1,49 @@ +import { WorkflowType } from "sil.appbuilder.portal.common/prisma"; + +export enum ProductActionType { + Rebuild = 'rebuild', + Republish = 'republish', + Cancel = 'cancel' +} + +export function getProductActions( + product: { + WorkflowInstance: { + WorkflowDefinition: { + Type: WorkflowType; + }; + } | null; + DatePublished: unknown; + ProductDefinition: { RebuildWorkflowId: unknown; RepublishWorkflowId: unknown }; + }, + projectOwnerId: number, + userId: number +) { + const ret: ProductActionType[] = []; + if (!product.WorkflowInstance) { + if (product.DatePublished) { + if (product.ProductDefinition.RebuildWorkflowId !== null) { + ret.push(ProductActionType.Rebuild); + } + if (product.ProductDefinition.RepublishWorkflowId !== null) { + ret.push(ProductActionType.Republish); + } + } + } else if ( + projectOwnerId === userId && + product.WorkflowInstance.WorkflowDefinition.Type !== WorkflowType.Startup + ) { + ret.push(ProductActionType.Cancel); + } + + return ret; +} + +export async function getFileInfo(url: string) { + const res = await fetch(url, { method: 'HEAD' }); + return { + contentType: res.headers.get('Content-Type'), + lastModified: new Date(res.headers.get('Last-Modified') ?? 0).toISOString(), + fileSize: res.headers.get('Content-Type') !== 'text/html'? res.headers.get('Content-Length') : null + } +} \ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/lib/products/server.ts b/source/SIL.AppBuilder.Portal/src/lib/products/server.ts new file mode 100644 index 0000000000..8838510c2f --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/products/server.ts @@ -0,0 +1,130 @@ +import { BullMQ, DatabaseWrites, prisma, Queues, Workflow } from 'sil.appbuilder.portal.common'; +import { ProductTransitionType, WorkflowType } from 'sil.appbuilder.portal.common/prisma'; +import { ProductActionType } from '.'; + +export async function doProductAction(productId: string, action: ProductActionType) { + const product = await prisma.products.findUnique({ + where: { + Id: productId + }, + select: { + Id: true, + ProjectId: true, + ProductDefinition: { + select: { + RebuildWorkflow: { + select: { + Type: true, + ProductType: true, + WorkflowOptions: true + } + }, + RepublishWorkflow: { + select: { + Type: true, + ProductType: true, + WorkflowOptions: true + } + } + } + }, + WorkflowInstance: { + select: { + WorkflowDefinition: { + select: { Type: true } + } + } + } + } + }); + + if (product) { + switch (action) { + case ProductActionType.Rebuild: + case ProductActionType.Republish: { + const flowType = action === 'rebuild' ? 'RebuildWorkflow' : 'RepublishWorkflow'; + if (product.ProductDefinition[flowType] && !product.WorkflowInstance) { + await Workflow.create(productId, { + productType: product.ProductDefinition[flowType].ProductType, + options: new Set(product.ProductDefinition[flowType].WorkflowOptions), + workflowType: product.ProductDefinition[flowType].Type + }); + } + break; + } + case ProductActionType.Cancel: + if ( + product.WorkflowInstance?.WorkflowDefinition && + product.WorkflowInstance.WorkflowDefinition.Type !== WorkflowType.Startup + ) { + await Queues.UserTasks.add( + `Delete UserTasks for canceled workflow (product #${productId})`, + { + type: BullMQ.JobType.UserTasks_Modify, + scope: 'Product', + productId, + operation: { + type: BullMQ.UserTasks.OpType.Delete + } + } + ); + await DatabaseWrites.productTransitions.create({ + data: { + ProductId: productId, + // This is how S1 does it. May want to change later + AllowedUserNames: '', + DateTransition: new Date(), + TransitionType: ProductTransitionType.CancelWorkflow, + WorkflowType: product.WorkflowInstance.WorkflowDefinition.Type + } + }); + await DatabaseWrites.workflowInstances.delete(productId, product.ProjectId); + } + break; + } + } +} + +/** + * Get the most recent published file of specified type associated with this product + * @param id Product ID + * @param type ProductArtifact type to be returned + */ +export async function getPublishedFile(productId: string, type: string) { + const publications = await prisma.productPublications.findMany({ + where: { + ProductId: productId, + Success: true + }, + include: { + ProductBuild: { + include: { + ProductArtifacts: { + select: { + ArtifactType: true, + Url: true + } + } + } + } + }, + orderBy: { + Id: 'desc' + } + }); + for (const publication of publications) { + if (!publication.ProductBuild.ProductArtifacts.length) { + continue; + } + const artifact = publication.ProductBuild.ProductArtifacts.find( + (pa) => pa.ArtifactType === type + ); + + if (artifact) { + return artifact; + } + } + + // Return null if product has not been successfully published + return null; +} diff --git a/source/SIL.AppBuilder.Portal/src/lib/projects/common.server.ts b/source/SIL.AppBuilder.Portal/src/lib/projects/common.server.ts new file mode 100644 index 0000000000..123ae0abdf --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/projects/common.server.ts @@ -0,0 +1,157 @@ +import { hasRoleForOrg, isAdminForOrg } from '$lib/utils'; +import type { Session } from '@auth/sveltekit'; +import type { Prisma } from '@prisma/client'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { RoleId } from 'sil.appbuilder.portal.common/prisma'; +import { canClaimProject, canModifyProject, type ProjectForAction } from './common'; + +export async function verifyCanViewAndEdit(user: Session, projectId: number) { + // Editing is allowed if the user owns the project, or if the user is an organization + // admin for the project's organization, or if the user is a super admin + const project = await prisma.projects.findUnique({ + where: { + Id: projectId + }, + select: { + Id: true, + OwnerId: true, + OrganizationId: true + } + }); + if (!project) return false; + return canModifyProject(user, project.OwnerId, project.OrganizationId); +} + +export function projectFilter(args: { + organizationId: number | null; + langCode: string; + productDefinitionId: number | null; + dateUpdatedRange: [Date, Date | null] | null; + search: string; +}): Prisma.ProjectsWhereInput { + return { + OrganizationId: args.organizationId !== null ? args.organizationId : undefined, + Language: args.langCode + ? { + contains: args.langCode, + mode: 'insensitive' + } + : undefined, + Products: + args.productDefinitionId !== null + ? { + some: { + ProductDefinitionId: args.productDefinitionId + } + } + : undefined, + AND: [ + { + OR: + args.dateUpdatedRange && args.dateUpdatedRange[1] + ? [ + { DateUpdated: null }, + { + DateUpdated: { + gt: args.dateUpdatedRange[0], + lt: args.dateUpdatedRange[1] + } + } + ] + : undefined + }, + { + OR: args.search + ? [ + { + Name: { + contains: args.search, + mode: 'insensitive' + } + }, + { + Language: { + contains: args.search, + mode: 'insensitive' + } + }, + { + Owner: { + Name: { + contains: args.search, + mode: 'insensitive' + } + } + }, + { + Organization: { + Name: { + contains: args.search, + mode: 'insensitive' + } + } + }, + { + Group: { + Name: { + contains: args.search, + mode: 'insensitive' + } + } + } + ] + : undefined + } + ] + }; +} +export async function verifyCanCreateProject(user: Session, orgId: number) { + // Creating a project is allowed if the user is an OrgAdmin or AppBuilder for the organization or a SuperAdmin + return ( + isAdminForOrg(orgId, user.user.roles) || + hasRoleForOrg(RoleId.AppBuilder, orgId, user.user.roles) + ); +} + +export async function userGroupsForOrg(userId: number, orgId: number) { + return prisma.groupMemberships.findMany({ + where: { + UserId: userId, + Group: { + is: { + OwnerId: orgId + } + } + }, + select: { + GroupId: true + } + }); +} + +export async function doProjectAction( + operation: string | null, + project: Omit, + session: Session, + orgId: number, + groups: number[] +) { + if (operation === 'archive' && !project?.DateArchived) { + await DatabaseWrites.projects.update(project.Id, { + DateArchived: new Date() + }); + // TODO: Delete UserTasks for Archived Project? + } else if (operation === 'reactivate' && !!project?.DateArchived) { + await DatabaseWrites.projects.update(project.Id, { + DateArchived: null + }); + // TODO: Create UserTasks for Reactivated Project? + } else if ( + operation === 'claim' && + canClaimProject(session, project?.OwnerId, orgId, project?.GroupId, groups) + ) { + await DatabaseWrites.projects.update(project.Id, { + OwnerId: session.user.userId + }); + } +} diff --git a/source/SIL.AppBuilder.Portal/src/lib/projects/common.ts b/source/SIL.AppBuilder.Portal/src/lib/projects/common.ts new file mode 100644 index 0000000000..0ef935410e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/projects/common.ts @@ -0,0 +1,216 @@ +import { paginateSchema } from '$lib/table'; +import { isAdminForOrg, isSuperAdmin } from '$lib/utils'; +import { idSchema } from '$lib/valibot'; +import type { Session } from '@auth/sveltekit'; +import type { Prisma } from '@prisma/client'; +import * as v from 'valibot'; + +export function pruneProjects( + projects: Prisma.ProjectsGetPayload<{ + include: { + Products: { + include: { + ProductDefinition: true; + WorkflowInstance: true; + }; + }; + Owner: true; + Group: true; + Organization: true; + }; + }>[] +) { + return projects.map( + ({ + Name, + Id, + Language, + Owner: { Name: OwnerName, Id: OwnerId }, + Organization: { Name: OrganizationName }, + Group: { Name: GroupName, Id: GroupId }, + DateActive, + DateUpdated, + DateArchived, + Products + }) => ({ + Name, + Id, + Language, + OwnerId, + OwnerName, + OrganizationName, + GroupName, + GroupId, + DateUpdated, + DateActive, + DateArchived, + Products: Products.map( + ({ Id, ProductDefinition, VersionBuilt, DateBuilt, WorkflowInstance, DatePublished }) => ({ + Id: Id, + ProductDefinitionId: ProductDefinition.Id, + ProductDefinitionName: ProductDefinition.Name, + VersionBuilt, + DateBuilt, + CanRebuild: !!( + !WorkflowInstance && + DatePublished && + ProductDefinition.RebuildWorkflowId !== null + ), + CanRepublish: !!( + !WorkflowInstance && + DatePublished && + ProductDefinition.RepublishWorkflowId !== null + ) + }) + ) + }) + ); +} + +export type PrunedProject = ReturnType[0]; + +export const projectSearchSchema = v.object({ + organizationId: v.nullable(idSchema), + langCode: v.string(), + productDefinitionId: v.nullable(idSchema), + dateUpdatedRange: v.nullable(v.tuple([v.date(), v.nullable(v.date())])), + page: paginateSchema, + search: v.string() +}); + +//language tag regex sourced from: https://stackoverflow.com/a/60899733 +export const langtagRegex = new RegExp( + '^(' + + '(' + // grandfathered + /* irregular */ '(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)' + + '|' + + /* regular */ '(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)' + + ')' + + '|' + // langtag + '(' + + '(' + + //language + ('([A-Za-z]{2,3}(-' + + //extlang + '([A-Za-z]{3}(-[A-Za-z]{3}){0,2})' + + ')?)|[A-Za-z]{4}|[A-Za-z]{5,8})') + + '(-' + + '([A-Za-z]{4})' + + ')?' + //script + '(-' + + '([A-Za-z]{2}|[0-9]{3})' + + ')?' + //region + '(-' + + '([A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3})' + + ')*' + //variant + //extension + '(-' + + '(' + + /* singleton */ ('[0-9A-WY-Za-wy-z]' + '(-[A-Za-z0-9]{2,8})+)') + + ')*' + + '(-' + + '(x(-[A-Za-z0-9]{1,8})+)' + + ')?' + //private use + ')' + + '|' + + '(x(-[A-Za-z0-9]{1,8})+)' + + ')$' +); + +const projectSchemaBase = v.object({ + Name: v.pipe(v.string(), v.minLength(1)), + Description: v.optional(v.string()), + Language: v.pipe( + v.string(), + v.regex(langtagRegex, (issue) => `Invalid BCP 47 Language Tag: ${issue.input}`) + ), + IsPublic: v.boolean() +}); + +export const projectCreateSchema = v.object({ + ...projectSchemaBase.entries, + group: idSchema, + type: idSchema +}); + +export const importJSONSchema = v.object({ + Projects: v.pipe( + v.array( + v.object({ + ...projectSchemaBase.entries, + AllowDownloads: v.optional(v.boolean()), + AutomaticBuilds: v.optional(v.boolean()) + }) + ), + v.minLength(1) + ), + Products: v.pipe( + v.array( + v.object({ + Name: v.string(), + Store: v.string() + }) + ), + v.minLength(1) + ) +}); + +export const projectActionSchema = v.object({ + operation: v.nullable(v.picklist(['archive', 'reactivate', 'claim'])), + // used to distinguish between single and bulk. will be null if bulk + projectId: v.nullable(idSchema) +}); + +export const bulkProjectActionSchema = v.object({ + ...projectActionSchema.entries, + projects: v.array(idSchema) +}); + +export type ProjectActionSchema = typeof projectActionSchema; + +export type ProjectForAction = { + Id: number; + Name: string | null; + OwnerId: number; + GroupId: number; + DateArchived: Date | null; +}; + +export function canModifyProject( + user: Session | null | undefined, + projectOwnerId: number, + organizationId: number +) { + return projectOwnerId === user?.user.userId || isAdminForOrg(organizationId, user?.user.roles); +} + +export function canClaimProject( + session: Session | null | undefined, + projectOwnerId: number, + organizationId: number, + projectGroupId: number, + userGroupIds: number[] +) { + if (session?.user.userId === projectOwnerId) return false; + if (isSuperAdmin(session?.user.roles)) return true; + return ( + canModifyProject(session, projectOwnerId, organizationId) && + userGroupIds.includes(projectGroupId) + ); +} + +export function canArchive( + project: Pick, + session: Session | null | undefined, + orgId: number +): boolean { + return !project.DateArchived && canModifyProject(session, project.OwnerId, orgId); +} + +export function canReactivate( + project: Pick, + session: Session | null | undefined, + orgId: number +): boolean { + return !!project.DateArchived && canModifyProject(session, project.OwnerId, orgId); +} diff --git a/source/SIL.AppBuilder.Portal/src/lib/projects/components/ProjectActionMenu.svelte b/source/SIL.AppBuilder.Portal/src/lib/projects/components/ProjectActionMenu.svelte new file mode 100644 index 0000000000..707183be3c --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/projects/components/ProjectActionMenu.svelte @@ -0,0 +1,103 @@ + + + close()} /> + + diff --git a/source/SIL.AppBuilder.Portal/src/lib/projects/components/ProjectCard.svelte b/source/SIL.AppBuilder.Portal/src/lib/projects/components/ProjectCard.svelte new file mode 100644 index 0000000000..12e43b7d52 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/projects/components/ProjectCard.svelte @@ -0,0 +1,125 @@ + + +
+
+ + {@render select?.()} + + + {project.Name} + + +
+ + + + + {project.Language} + + + {@render actions?.()} +
+
+
+ + + {project.OwnerName} + + + + {project.OrganizationName} + + + + + {project.GroupName} + + +
+
+ + + {m.projectTable_columns_updatedOn()}: + + + {getTimeDateString(project.DateUpdated)} + + + + + {m.projectTable_columns_activeSince()}: + + + {getTimeDateString(project.DateActive)} + + +
+
+
+
+ {#if project.Products.length > 0} + + + + + + + + + + {#each project.Products as product} + + + + + + {/each} + +
{m.projectTable_products()}{m.projectTable_columns_buildVersion()}{m.projectTable_columns_buildDate()}
+
+ + {product.ProductDefinitionName} +
+
+ {product.VersionBuilt ?? '-'} + + {getTimeDateString(product.DateBuilt)} +
+ {:else} +

{m.projectTable_noProducts()}

+ {/if} +
+
+ + \ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/lib/projects/components/ProjectFilterSelector.svelte b/source/SIL.AppBuilder.Portal/src/lib/projects/components/ProjectFilterSelector.svelte new file mode 100644 index 0000000000..5de8ffede4 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/projects/components/ProjectFilterSelector.svelte @@ -0,0 +1,49 @@ + + + + + diff --git a/source/SIL.AppBuilder.Portal/src/lib/springyGraph.ts b/source/SIL.AppBuilder.Portal/src/lib/springyGraph.ts new file mode 100644 index 0000000000..31ccf06368 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/springyGraph.ts @@ -0,0 +1,797 @@ +/** + * Heavily based on: https://github.com/dhotson/springy/ + * + * Modifications: + * Updated to modern TypeScript + * + * The original license of the inpiring code is included below. + * + * Springy v2.7.1 + * + * Copyright (c) 2010-2013 Dennis Hotson + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ +// TODO: Using a physics simulation for our graph is overkill and slow. Redoing this would be optimal. +export namespace Springy { + export type NodeData = { + mass?: number; + label?: string; + static?: Physics.Vector; // static position + }; + + export type Node = { + id: string; + data?: NodeData; + }; + + export type EdgeData = { + length?: number; + type?: any; + }; + + export type Edge = { + id: number; + source: Node; + target: Node; + directed?: boolean; + data?: EdgeData; + }; + + export class Graph { + nodeSet: { [key: string]: Node }; + nodes: Node[]; + edges: Edge[]; + adjacency: { [key: string]: { [key: string]: Edge[] } }; + + nextEdgeId: number; + eventListeners: any[]; + + constructor() { + this.nodeSet = {}; + this.nodes = []; + this.edges = []; + this.adjacency = {}; + + this.nextEdgeId = 0; + this.eventListeners = []; + } + + addNode(node: Node): Node { + if (!(node.id in this.nodeSet)) { + this.nodes.push(node); + } + + this.nodeSet[node.id] = node; + + this.notify(); + return node; + } + + addNodes(args: string[]) { + // accepts variable number of arguments, where each argument + // is a string that becomes both node identifier and label + for (let i = 0; i < args.length; i++) { + const name = args[i]; + const node: Node = { id: name, data: { label: name } }; + this.addNode(node); + } + } + + addNodeData(id: string, data: NodeData) { + this.nodeSet[id].data = data; + } + + addEdge(edge: Edge): Edge { + let exists = false; + this.edges.forEach(function (e) { + if (edge.id === e.id) { + exists = true; + } + }); + + if (!exists) { + this.edges.push(edge); + } + + if (!(edge.source.id in this.adjacency)) { + this.adjacency[edge.source.id] = {}; + } + if (!(edge.target.id in this.adjacency[edge.source.id])) { + this.adjacency[edge.source.id][edge.target.id] = []; + } + + exists = false; + this.adjacency[edge.source.id][edge.target.id].forEach((e) => { + if (edge.id === e.id) { + exists = true; + } + }); + + if (!exists) { + this.adjacency[edge.source.id][edge.target.id].push(edge); + } + + this.notify(); + return edge; + } + + addEdges(args: { source: string; target: string; data?: EdgeData }[]) { + for (var i = 0; i < args.length; i++) { + const e = args[i]; + const node1 = this.nodeSet[e.source]; + if (node1 == undefined) { + throw new TypeError('invalid node name: "' + e.source + '" (source)'); + } + var node2 = this.nodeSet[e.target]; + if (node2 == undefined) { + throw new TypeError('invalid node name: "' + e.target + '" (target)'); + } + + this.newEdge(node1, node2, e.data); + } + } + + newNode(id: string, data?: NodeData): Node { + return this.addNode({ id: id, data: data }); + } + + newEdge(source: Node, target: Node, data?: EdgeData) { + return this.addEdge({ id: this.nextEdgeId++, source: source, target: target, data: data }); + } + + /** + * add nodes and edges from JSON object + * + * Springy's simple JSON format for graphs. + * + * historically, Springy uses separate lists of nodes and edges: + * + * { + * "nodes": [ + * "center", + * "left", + * "right", + * "up", + * "satellite" + * ], + * "edges": [ + * ["center", "left"], + * ["center", "right"], + * ["center", "up"] + * ] + * } + * + **/ + loadJSON(json: string | { nodes: string[]; edges: string[][] }) { + const obj = typeof json === 'string' ? JSON.parse(json) : json; + + if ('nodes' in obj || 'edges' in obj) { + this.addNodes(obj.nodes); + this.addEdges( + obj.edges.map((e: string[]) => { + return { + source: e[0], + target: e[1] + }; + }) + ); + } + } + + /** find the edges from node1 to node2 */ + getEdges(node1: Node, node2: Node): Edge[] { + if (node1.id in this.adjacency && node2.id in this.adjacency[node1.id]) { + return this.adjacency[node1.id][node2.id]; + } + return []; + } + + /** remove a node and its associated edges from the graph */ + removeNode(node: Node) { + if (node.id in this.nodeSet) { + delete this.nodeSet[node.id]; + } + + for (let i = this.nodes.length - 1; i >= 0; i--) { + if (this.nodes[i].id === node.id) { + this.nodes.splice(i, 1); + } + } + + this.detachNode(node); + } + + /** removes edges associated with a given node */ + detachNode(node: Node) { + const tmpEdges = this.edges.slice(); + tmpEdges.forEach((e) => { + if (e.source.id === node.id || e.target.id === node.id) { + this.removeEdge(e); + } + }); + + this.notify(); + } + + /** remove a node and it's associated edges from the graph */ + removeEdge(edge: Edge) { + for (let i = this.edges.length - 1; i >= 0; i--) { + if (this.edges[i].id === edge.id) { + this.edges.splice(i, 1); + } + } + + for (let x in this.adjacency) { + for (let y in this.adjacency[x]) { + const edges = this.adjacency[x][y]; + + for (let j = edges.length - 1; j >= 0; j--) { + if (this.adjacency[x][y][j].id === edge.id) { + this.adjacency[x][y].splice(j, 1); + } + } + + // Clean up empty edge arrays + if (this.adjacency[x][y].length == 0) { + delete this.adjacency[x][y]; + } + } + + // Clean up empty objects + if (isEmpty(this.adjacency[x])) { + delete this.adjacency[x]; + } + } + + this.notify(); + } + + /** Merge a list of nodes and edges into the current graph. eg. */ + merge(data: { nodes: Node[]; edges: Edge[] }) { + const nodes: { [key: string]: Node } = {}; + data.nodes.forEach((n) => { + nodes[n.id] = this.addNode({ id: n.id, data: n.data }); + }); + + data.edges.forEach((e) => { + const from = nodes[e.source.id]; + const to = nodes[e.target.id]; + const edge = this.addEdge({ + id: this.nextEdgeId++, + source: from, + target: to, + data: e.data + }); + }, this); + } + + /** Remove node if filter returns true */ + filterNodes(filter: (node: Node) => boolean) { + const tmpNodes = this.nodes.slice(); + tmpNodes.forEach((n) => { + if (!filter(n)) { + this.removeNode(n); + } + }); + } + + /** Remove edge if filter returns true */ + filterEdges(filter: (edge: Edge) => boolean) { + const tmpEdges = this.edges.slice(); + tmpEdges.forEach((e) => { + if (!filter(e)) { + this.removeEdge(e); + } + }); + } + + subscribe(cb: (graph?: Graph) => void) { + this.eventListeners.push(cb); + } + + notify() { + this.eventListeners.forEach((cb) => { + cb(this); + }); + } + } + + export namespace Physics { + export class Vector { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + add(v2: Vector) { + return new Vector(this.x + v2.x, this.y + v2.y); + } + + subtract(v2: Vector) { + return new Vector(this.x - v2.x, this.y - v2.y); + } + + multiply(n: number) { + return new Vector(this.x * n, this.y * n); + } + + divide(n: number) { + return new Vector(this.x / n || 0, this.y / n || 0); // Avoid divide by zero errors.. + } + + magnitude() { + return Math.sqrt(this.x * this.x + this.y * this.y); + } + + normal() { + return new Vector(-this.y, this.x); + } + + normalise() { + return this.divide(this.magnitude()); + } + + static random() { + return new Vector(10.0 * (Math.random() - 0.5), 10.0 * (Math.random() - 0.5)); + } + + translateToScreenSpace(offset: Vector, scale: number | Vector) { + const sx = typeof scale === 'number' ? scale : scale.x; + const sy = typeof scale === 'number' ? scale : scale.y; + return new Vector(offset.x + this.x * sx, offset.y + this.y * sy); + } + } + + export class Point { + p: Vector; // position + m: number; // mass + v: Vector; // velocity + a: Vector; // acceleration + fixed: boolean; + + constructor(position: Vector, mass: number, fixed: boolean = false) { + this.p = position; // position + this.m = mass; // mass + this.v = new Vector(0, 0); // velocity + this.a = new Vector(0, 0); // acceleration + this.fixed = fixed; + } + + applyForce(force: Vector) { + if (this.fixed) return; // don't apply force if fixed + this.a = this.a.add(force.divide(this.m)); + } + } + + export class Spring { + point1: Point; + point2: Point; + length: number; // spring length at rest + k: number; // spring constant (See Hooke's law) .. how stiff the spring is + + constructor(point1: Point, point2: Point, length: number, k: number) { + this.point1 = point1; + this.point2 = point2; + this.length = length; // spring length at rest + this.k = k; // spring constant (See Hooke's law) .. how stiff the spring is + } + + distanceToPoint(point: Point) { + // hardcore vector arithmetic.. ohh yeah! + // .. see http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment/865080#865080 + var n = this.point2.p.subtract(this.point1.p).normalise().normal(); + var ac = point.p.subtract(this.point1.p); + return Math.abs(ac.x * n.x + ac.y * n.y); + } + } + } + + export class Layout { + graph: Graph; + _started: boolean = false; + _stop: boolean = false; + /** keep track of points associated with nodes */ + nodePoints: { [key: string]: Physics.Point }; + /** keep track of springs associated with edges */ + edgeSprings: { [key: number]: Physics.Spring }; + /** spring stiffness constant */ + stiffness: number; + + constructor(graph: Graph, stiffness: number) { + this.graph = graph; + + this.nodePoints = {}; + this.edgeSprings = {}; + this.stiffness = stiffness; + } + + point(node: Node) { + if (!(node.id in this.nodePoints)) { + var mass = node.data?.mass !== undefined ? node.data.mass : 1.0; + this.nodePoints[node.id] = new Physics.Point( + node.data?.static ? node.data.static : Physics.Vector.random(), + mass, + node.data?.static !== undefined + ); + } + + return this.nodePoints[node.id]; + } + + spring(edge: Edge) { + if (!(edge.id in this.edgeSprings)) { + const length = edge.data?.length !== undefined ? edge.data.length : 1.0; + + let existingSpring: Physics.Spring | undefined; + + const from = this.graph.getEdges(edge.source, edge.target); + from.forEach((e) => { + if (!existingSpring && e.id in this.edgeSprings) { + existingSpring = this.edgeSprings[e.id]; + return new Physics.Spring(existingSpring.point1, existingSpring.point2, 0.0, 0.0); + } + }); + + const to = this.graph.getEdges(edge.target, edge.source); + to.forEach((e) => { + if (!existingSpring && e.id in this.edgeSprings) { + existingSpring = this.edgeSprings[e.id]; + return new Physics.Spring(existingSpring.point2, existingSpring.point1, 0.0, 0.0); + } + }); + + this.edgeSprings[edge.id] = new Physics.Spring( + this.point(edge.source), + this.point(edge.target), + length, + this.stiffness + ); + } + + return this.edgeSprings[edge.id]; + } + + eachNode(callback: (node: Node, point: Physics.Point) => void) { + var t = this; + this.graph.nodes.forEach(function (n) { + callback.call(t, n, t.point(n)); + }); + } + + eachEdge(callback: (edge: Edge, spring: Physics.Spring) => void) { + var t = this; + this.graph.edges.forEach(function (e) { + callback.call(t, e, t.spring(e)); + }); + } + + eachSpring(callback: (spring: Physics.Spring) => void) { + var t = this; + this.graph.edges.forEach(function (e) { + callback.call(t, t.spring(e)); + }); + } + + /** + * Start simulation if it's not running already. + * In case it's running then the call is ignored, and none of the callbacks passed is ever executed. + */ + start( + render?: () => void, + onRenderStop?: () => void, + onRenderStart?: () => void, + tick?: () => void, + stopCondition?: () => boolean + ) { + var t = this; + + if (this._started) return; + this._started = true; + this._stop = false; + + if (onRenderStart) { + onRenderStart(); + } + + requestAnimationFrame(function step() { + if (tick) { + tick(); + } + + if (render) { + render(); + } + + // stop simulation when energy of the system goes below a threshold + if (t._stop || (stopCondition && stopCondition())) { + t._started = false; + if (onRenderStop) { + onRenderStop(); + } + } else { + requestAnimationFrame(step); + } + }); + } + + stop() { + this._stop = true; + } + } + + export class ForceDirected extends Layout { + /** repulsion constant */ + repulsion: number; + /** velocity damping factor */ + damping: number; + /** threshold used to determine render stop */ + minEnergyThreshold: number; + /** nodes aren't allowed to exceed this speed */ + maxSpeed: number; + + constructor( + graph: Graph, + stiffness: number, + repulsion: number, + damping: number, + minEnergyThreshold: number = 0.01, + maxSpeed: number = Infinity + ) { + super(graph, stiffness); + this.repulsion = repulsion; + this.damping = damping; + this.minEnergyThreshold = minEnergyThreshold; + this.maxSpeed = maxSpeed; + } + + // Physics stuff + applyCoulombsLaw() { + this.eachNode((n1, point1) => { + this.eachNode((n2, point2) => { + if (point1 !== point2) { + var d = point1.p.subtract(point2.p); + var distance = d.magnitude() + 0.1; // avoid massive forces at small distances (and divide by zero) + var direction = d.normalise(); + + // apply force to each end point + point1.applyForce(direction.multiply(this.repulsion).divide(distance * distance * 0.5)); + point2.applyForce( + direction.multiply(this.repulsion).divide(distance * distance * -0.5) + ); + } + }); + }); + } + + applyHookesLaw() { + this.eachSpring((spring) => { + var d = spring.point2.p.subtract(spring.point1.p); // the direction of the spring + var displacement = spring.length - d.magnitude(); + var direction = d.normalise(); + + // apply force to each end point + spring.point1.applyForce(direction.multiply(spring.k * displacement * -0.5)); + spring.point2.applyForce(direction.multiply(spring.k * displacement * 0.5)); + }); + } + + attractToCentre() { + this.eachNode((node, point) => { + var direction = point.p.multiply(-1.0); + point.applyForce(direction.multiply(this.repulsion / 50.0)); + }); + } + + updateVelocity(timestep: number) { + this.eachNode((node, point) => { + point.v = point.v.add(point.a.multiply(timestep)).multiply(this.damping); + if (point.v.magnitude() > this.maxSpeed) { + point.v = point.v.normalise().multiply(this.maxSpeed); + } + point.a = new Physics.Vector(0, 0); + }); + } + + updatePosition(timestep: number) { + this.eachNode((node, point) => { + point.p = point.p.add(point.v.multiply(timestep)); + }); + } + + // Calculate the total kinetic energy of the system + totalEnergy() { + var energy = 0.0; + this.eachNode(function (node, point) { + var speed = point.v.magnitude(); + energy += 0.5 * point.m * speed * speed; + }); + + return energy; + } + + /** + * Start simulation if it's not running already. + * In case it's running then the call is ignored, and none of the callbacks passed is ever executed. + */ + start( + render?: () => void, + onRenderStop?: () => void, + onRenderStart?: () => void, + tick?: () => void, + stopCondition?: () => boolean + ) { + super.start( + render, + onRenderStop, + onRenderStart, + () => { + this.tick(0.03); + }, + () => this.totalEnergy() < this.minEnergyThreshold + ); + } + + tick(timestep: number) { + this.applyCoulombsLaw(); + this.applyHookesLaw(); + this.attractToCentre(); + this.updateVelocity(timestep); + this.updatePosition(timestep); + } + + // Find the nearest point to a particular position + nearest(pos: Physics.Vector) { + var min: { node: Node; point: Physics.Point; distance: number } | undefined; + var t = this; + this.graph.nodes.forEach(function (n) { + var point = t.point(n); + var distance = point.p.subtract(pos).magnitude(); + + if (min?.distance === undefined || distance < min.distance) { + min = { node: n, point: point, distance: distance }; + } + }); + + return min; + } + + getBoundingBox() { + var bottomleft = new Physics.Vector(-2, -2); + var topright = new Physics.Vector(2, 2); + + this.eachNode(function (n, point) { + if (point.p.x < bottomleft.x) { + bottomleft.x = point.p.x; + } + if (point.p.y < bottomleft.y) { + bottomleft.y = point.p.y; + } + if (point.p.x > topright.x) { + topright.x = point.p.x; + } + if (point.p.y > topright.y) { + topright.y = point.p.y; + } + }); + + var padding = topright.subtract(bottomleft).multiply(0.07); // ~5% padding + + return { bottomleft: bottomleft.subtract(padding), topright: topright.add(padding) }; + } + } + + /** + * Renderer handles the layout rendering loop + * @param onRenderStop optional callback function that gets executed whenever rendering stops. + * @param onRenderStart optional callback function that gets executed whenever rendering starts. + * @param onRenderFrame optional callback function that gets executed after each frame is rendered. + */ + export class Renderer { + layout: Layout; + clear: () => void; + drawEdge: (edge: Edge, source: Physics.Vector, target: Physics.Vector) => void; + drawNode: (node: Node, position: Physics.Vector) => void; + onRenderStop: () => void; + onRenderStart: () => void; + onRenderFrame: () => void; + + constructor( + layout: Layout, + clear: () => void, + drawEdge: (edge: Edge, source: Physics.Vector, target: Physics.Vector) => void, + drawNode: (node: Node, position: Physics.Vector) => void, + onRenderStop: () => void, + onRenderStart: () => void, + onRenderFrame: () => void + ) { + this.layout = layout; + this.clear = clear; + this.drawEdge = drawEdge; + this.drawNode = drawNode; + this.onRenderStop = onRenderStop; + this.onRenderStart = onRenderStart; + this.onRenderFrame = onRenderFrame; + + this.layout.graph.subscribe((e) => { + this.graphChanged(); + }); + } + + /** + * Starts the simulation of the layout in use. + * + * Note that in case the algorithm is still or already running then the layout that's in use + * might silently ignore the call, and your optional done callback is never executed. + * At least the built-in ForceDirected layout behaves in this way. + * + * @param done An optional callback function that gets executed when the springy algorithm stops, + * either because it ended or because stop() was called. + */ + start(done?: () => void) { + var t = this; + this.layout.start( + function render() { + t.clear(); + + t.layout.eachEdge(function (edge, spring) { + t.drawEdge(edge, spring.point1.p, spring.point2.p); + }); + + t.layout.eachNode(function (node, point) { + t.drawNode(node, point.p); + }); + + if (t.onRenderFrame !== undefined) { + t.onRenderFrame(); + } + }, + done + ? () => { + this.onRenderStop(); + done(); + } + : this.onRenderStop, + this.onRenderStart + ); + } + + stop() { + this.layout.stop(); + } + + graphChanged() { + this.start(); + } + } + + function isEmpty(obj: any) { + for (var k in obj) { + if (obj.hasOwnProperty(k)) { + return false; + } + } + return true; + } +} diff --git a/source/SIL.AppBuilder.Portal/src/lib/stores.ts b/source/SIL.AppBuilder.Portal/src/lib/stores.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/SIL.AppBuilder.Portal/src/lib/table.ts b/source/SIL.AppBuilder.Portal/src/lib/table.ts new file mode 100644 index 0000000000..5239251b14 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/table.ts @@ -0,0 +1,8 @@ +import * as v from 'valibot'; + +export const paginateSchema = v.object({ + page: v.number(), + size: v.number() +}); + +export type PaginateSchema = typeof paginateSchema; \ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/lib/timeUtils.ts b/source/SIL.AppBuilder.Portal/src/lib/timeUtils.ts new file mode 100644 index 0000000000..0e24b6328e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/timeUtils.ts @@ -0,0 +1,43 @@ +import { languageTag } from './paraglide/runtime'; + +let langtag = languageTag(); +let relativeTimeFormatter = new Intl.RelativeTimeFormat(langtag); +export function getRelativeTime(date: Date | null) { + if (!date) return '-'; + if (langtag !== languageTag()) { + langtag = languageTag(); + relativeTimeFormatter = new Intl.RelativeTimeFormat(langtag); + } + // in miliseconds + const units = { + year: 24 * 60 * 60 * 1000 * 365, + month: (24 * 60 * 60 * 1000 * 365) / 12, + day: 24 * 60 * 60 * 1000, + hour: 60 * 60 * 1000, + minute: 60 * 1000, + second: 1000 + } as const; + + const elapsed = date.valueOf() - new Date().valueOf(); + for (const u in units) { + if (Math.abs(elapsed) > units[u as keyof typeof units] || u == 'second') { + const ret = relativeTimeFormatter.format( + Math.round(elapsed / units[u as keyof typeof units]), + u as keyof typeof units + ); + return ret; + } + } + return 'ERROR'; +} +export function getTimeDateString(date: Date | null) { + return `${date?.toLocaleDateString(languageTag()) ?? '-'} ${ + date + ?.toLocaleTimeString(languageTag(), { + hour: 'numeric', + minute: '2-digit', + hour12: true + }) + .replace(' ', '\xa0') ?? '' + }`; +} diff --git a/source/SIL.AppBuilder.Portal/src/lib/utils.ts b/source/SIL.AppBuilder.Portal/src/lib/utils.ts new file mode 100644 index 0000000000..a6d535b747 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/utils.ts @@ -0,0 +1,70 @@ +import { RoleId } from 'sil.appbuilder.portal.common/prisma'; + +export function bytesToHumanSize(bytes: bigint | null) { + if (bytes === null) { + return '--'; + } + const base = BigInt('1024'); + if (bytes > base ** BigInt(3)) { + return bytes / base ** BigInt(3) + ' GB'; + } else if (bytes > base * base) { + return bytes / (base * base) + ' MB'; + } else if (bytes > base) { + return bytes / base + ' KB'; + } else { + return bytes + ' bytes'; + } +} + +interface NamedEntity { + Name: string | null; +} + +export function sortByName(a: NamedEntity, b: NamedEntity, languageTag: string): number { + return a.Name?.localeCompare(b.Name ?? '', languageTag) ?? 0; +} + +/** returns true if user is a SuperAdmin, or is an OrgAdmin for the specified organization + * @param roles [RoleId, OrganizationId][] + */ +export function isAdminForOrg(orgId: number, roles?: [number, number][]): boolean { + return !!roles?.find( + (r) => r[0] === RoleId.SuperAdmin || (r[0] === RoleId.OrgAdmin && r[1] === orgId) + ); +} +/** returns true if user is a SuperAdmin, or is an OrgAdmin for any of the provided orgs + * @param roles [RoleId, OrganizationId][] + */ +export function isAdminForOrgs(orgs: number[], roles?: [number, number][]): boolean { + return !!roles?.find( + (r) => r[0] === RoleId.SuperAdmin || (r[0] === RoleId.OrgAdmin && orgs.includes(r[1])) + ); +} +/** returns true if user is a SuperAdmin, or is an OrgAdmin for any organization + * @param roles [RoleId, OrganizationId][] + */ +export function isAdmin(roles?: [number, number][]): boolean { + return !!roles?.find((r) => r[0] === RoleId.SuperAdmin || r[0] === RoleId.OrgAdmin); +} +/** returns true if user is a SuperAdmin + * @param roles [RoleId, OrganizationId][] + */ +export function isSuperAdmin(roles?: [number, number][]): boolean { + return !!roles?.find((r) => r[0] === RoleId.SuperAdmin); +} +/** returns true if user has specified role in specified org + * + * IS NOT SHORT-CIRCUITED by SuperAdmin + * @param roles [RoleId, OrganizationId][] + */ +export function hasRoleForOrg(role: RoleId, orgId: number, roles?: [number, number][]): boolean { + return !!roles?.find((r) => r[0] === role && r[1] === orgId); +} +/** returns a list of organizations where the user has the specified role + * + * IS NOT SHORT-CIRCUITED by SuperAdmin + * @param roles [RoleId, OrganizationId][] +*/ +export function orgsForRole(role: RoleId, roles?: [number, number][]): number[] { + return roles?.filter((r) => r[0] === role).map((r) => r[1]) ?? []; +} diff --git a/source/SIL.AppBuilder.Portal/src/lib/valibot.ts b/source/SIL.AppBuilder.Portal/src/lib/valibot.ts new file mode 100644 index 0000000000..8c56dcc99c --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/valibot.ts @@ -0,0 +1,3 @@ +import * as v from 'valibot'; + +export const idSchema = v.pipe(v.number(), v.minValue(0), v.integer()); diff --git a/source/SIL.AppBuilder.Portal/src/params/idNumber.ts b/source/SIL.AppBuilder.Portal/src/params/idNumber.ts new file mode 100644 index 0000000000..01505eb8ca --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/params/idNumber.ts @@ -0,0 +1,4 @@ +export function match(param: string) { + const val = parseInt(param); + return '' + val === param && !isNaN(val) && val > 0; +} diff --git a/source/SIL.AppBuilder.Portal/src/params/projectSelector.ts b/source/SIL.AppBuilder.Portal/src/params/projectSelector.ts new file mode 100644 index 0000000000..894cbf4260 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/params/projectSelector.ts @@ -0,0 +1,3 @@ +export function match(param: string) { + return ['organization', 'archived', 'active', 'all', 'own'].includes(param); +} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/+layout.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/+layout.server.ts new file mode 100644 index 0000000000..545b09e3f3 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/+layout.server.ts @@ -0,0 +1,40 @@ +import { isSuperAdmin } from '$lib/utils'; +import { prisma } from 'sil.appbuilder.portal.common'; +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async (event) => { + const user = (await event.locals.auth())!.user; + const numberOfTasks = ( + await prisma.userTasks.findMany({ + where: { + UserId: user.userId + }, + select: { + Id: true + }, + distinct: 'ProductId' + }) + ).length; + const organizations = await prisma.organizations.findMany({ + where: isSuperAdmin(user.roles) + ? undefined + : { + OrganizationMemberships: { + some: { + UserId: user.userId + } + } + }, + select: { + Id: true, + Name: true, + LogoUrl: true, + Owner: { + select: { + Name: true + } + } + } + }); + return { organizations, numberOfTasks }; +}; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/+layout.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/+layout.svelte new file mode 100644 index 0000000000..db37dce339 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/+layout.svelte @@ -0,0 +1,248 @@ + + + + + {data.numberOfTasks + ? m.tabAppName_other({ count: data.numberOfTasks }) + : m.tabAppName_zero()}{dev ? ' - SvelteKit' : ''} + + + + + + + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/+layout.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/+layout.svelte new file mode 100644 index 0000000000..36668c5b7e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/+layout.svelte @@ -0,0 +1,33 @@ + + + + + {@render children?.()} + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/build-engines/+layout.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/build-engines/+layout.svelte new file mode 100644 index 0000000000..0220432f28 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/build-engines/+layout.svelte @@ -0,0 +1,8 @@ + + +

{m.admin_settings_buildEngines_title()}

+{@render children?.()} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/build-engines/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/build-engines/+page.server.ts new file mode 100644 index 0000000000..5270436cbc --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/build-engines/+page.server.ts @@ -0,0 +1,10 @@ +// src/routes/admin/settings/build-engines/+page.server.ts + +import { prisma } from 'sil.appbuilder.portal.common'; +import type { PageServerLoad } from './$types'; + +export const load = (async () => { + const buildEngines = await prisma.systemStatuses.findMany(); + + return { buildEngines }; +}) satisfies PageServerLoad; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/build-engines/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/build-engines/+page.svelte new file mode 100644 index 0000000000..6a48bb3d4e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/build-engines/+page.svelte @@ -0,0 +1,43 @@ + + +{#snippet date(engine?: (typeof data.buildEngines)[0])} + + {engine?.DateUpdated ? getRelativeTime(engine.DateUpdated) : '-'} + +{/snippet} + +
+ {#each data.buildEngines as buildEngine} + + {/each} +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/+layout.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/+layout.svelte new file mode 100644 index 0000000000..896c1a3d25 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/+layout.svelte @@ -0,0 +1,10 @@ + + +

{m.admin_settings_organizations_title()}

+{@render children?.()} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/+page.server.ts new file mode 100644 index 0000000000..23de134a3b --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/+page.server.ts @@ -0,0 +1,12 @@ +// src/routes/+page.server.ts + +import { prisma } from 'sil.appbuilder.portal.common'; +import type { PageServerLoad } from './$types'; + +export const load = (async () => { + const organizations = await prisma.organizations.findMany({ + include: { Owner: true } + }); + + return { organizations }; +}) satisfies PageServerLoad; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/+page.svelte new file mode 100644 index 0000000000..791004ea7d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/+page.svelte @@ -0,0 +1,43 @@ + + + + {m.admin_settings_organizations_add()} + + +
+ {#each data.organizations.sort((a, b) => sortByName(a, b, languageTag())) as organization} + goto('/admin/settings/organizations/edit?id=' + organization.Id)} + title={organization.Name} + fields={[ + { key: 'admin_settings_organizations_owner', value: organization.Owner.Name }, + { + key: 'admin_settings_organizations_websiteURL', + value: organization.WebsiteUrl + }, + { + key: 'admin_settings_organizations_buildEngineURL', + value: organization.BuildEngineUrl + }, + { + key: 'admin_settings_organizations_accessToken', + value: organization.BuildEngineApiAccessToken + } + ]} + /> + {/each} +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/edit/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/edit/+page.server.ts new file mode 100644 index 0000000000..f85b4cc7e6 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/edit/+page.server.ts @@ -0,0 +1,111 @@ +import { base } from '$app/paths'; +import { idSchema } from '$lib/valibot'; +import { fail, redirect } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const editSchema = v.object({ + id: idSchema, + name: v.nullable(v.string()), + owner: idSchema, + websiteURL: v.nullable(v.string()), + buildEngineURL: v.nullable(v.string()), + buildEngineAccessToken: v.nullable(v.string()), + logoURL: v.nullable(v.string()), + publicByDefault: v.boolean(), + stores: v.array( + v.object({ + storeId: idSchema, + enabled: v.boolean() + }) + ) +}); +export const load = (async ({ url }) => { + const id = parseInt(url.searchParams.get('id') ?? ''); + if (isNaN(id)) { + return redirect(302, base + '/admin/settings/organizations'); + } + const data = await prisma.organizations.findUnique({ + where: { + Id: id + } + }); + const orgStores = await prisma.organizationStores.findMany({ + where: { + OrganizationId: id + } + }); + if (!data) return redirect(302, base + '/admin/settings/organizations'); + const users = await prisma.users.findMany(); + const orgStoreNumList = new Set(orgStores.map((s) => s.StoreId)); + const stores = await prisma.stores.findMany(); + const enabledStores = stores.map((store) => ({ + storeId: store.Id, + enabled: orgStoreNumList.has(store.Id) + })); + const options = { users, stores }; + const form = await superValidate( + { + id: data.Id, + name: data.Name, + owner: data.OwnerId, + websiteURL: data.WebsiteUrl, + buildEngineURL: data.BuildEngineUrl, + buildEngineAccessToken: data.BuildEngineApiAccessToken, + logoURL: data.LogoUrl, + publicByDefault: data.PublicByDefault ?? false, + stores: enabledStores + }, + valibot(editSchema) + ); + return { form, options }; +}) satisfies PageServerLoad; + +export const actions = { + async edit({ cookies, request }) { + const form = await superValidate(request, valibot(editSchema)); + if (!form.valid) { + return fail(400, { form, ok: false, errors: form.errors }); + } + // return { ok: true, form }; + try { + const { + id, + name, + buildEngineAccessToken, + buildEngineURL, + logoURL, + owner, + publicByDefault, + websiteURL, + stores + } = form.data; + await DatabaseWrites.organizations.update({ + where: { + Id: id + }, + data: { + Name: name, + BuildEngineApiAccessToken: buildEngineAccessToken, + BuildEngineUrl: buildEngineURL, + LogoUrl: logoURL, + OwnerId: owner, + PublicByDefault: publicByDefault, + WebsiteUrl: websiteURL + } + }); + await DatabaseWrites.organizationStores.updateOrganizationStores( + id, + stores.filter((s) => s.enabled).map((s) => s.storeId) + ); + + return { ok: true, form }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/edit/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/edit/+page.svelte new file mode 100644 index 0000000000..16d247cc1d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/edit/+page.svelte @@ -0,0 +1,126 @@ + + + +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + {#each $superFormData.stores as store} + + {/each} + + {#if $allErrors.length} +
    + {#each $allErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} +
+ + Cancel +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/new/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/new/+page.server.ts new file mode 100644 index 0000000000..fc60116834 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/new/+page.server.ts @@ -0,0 +1,63 @@ +import { idSchema } from '$lib/valibot'; +import { fail } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const createSchema = v.object({ + name: v.nullable(v.string()), + websiteURL: v.nullable(v.string()), + buildEngineURL: v.nullable(v.string()), + buildEngineAccessToken: v.nullable(v.string()), + logoURL: v.nullable(v.string()), + useDefaultBuildEngine: v.boolean(), + publicByDefault: v.boolean(), + owner: idSchema +}); + +export const load = (async ({ url }) => { + const form = await superValidate(valibot(createSchema)); + const options = { + users: await prisma.users.findMany() + }; + return { form, options }; +}) satisfies PageServerLoad; + +export const actions = { + async new({ cookies, request }) { + const form = await superValidate(request, valibot(createSchema)); + if (!form.valid) { + return fail(400, { form, ok: false, errors: form.errors }); + } + try { + const { + buildEngineAccessToken, + buildEngineURL, + logoURL, + name, + owner, + publicByDefault, + useDefaultBuildEngine, + websiteURL + } = form.data; + await DatabaseWrites.organizations.create({ + data: { + Name: name, + BuildEngineApiAccessToken: buildEngineAccessToken, + BuildEngineUrl: buildEngineURL, + LogoUrl: logoURL, + OwnerId: owner, + PublicByDefault: publicByDefault, + UseDefaultBuildEngine: useDefaultBuildEngine, + WebsiteUrl: websiteURL + } + }); + return { ok: true, form }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/new/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/new/+page.svelte new file mode 100644 index 0000000000..ec1649ea7a --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/organizations/new/+page.svelte @@ -0,0 +1,102 @@ + + +

{m.newOrganization_title()}

+ +
+ + + + + + + + + + + + + + + + + + +
+ +
+ {#if $allErrors.length} +
    + {#each $allErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} +
+ + Cancel +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/+layout.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/+layout.svelte new file mode 100644 index 0000000000..96630d5763 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/+layout.svelte @@ -0,0 +1,10 @@ + + +

{m.admin_settings_productDefinitions_title()}

+{@render children?.()} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/+page.server.ts new file mode 100644 index 0000000000..e9437b8def --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/+page.server.ts @@ -0,0 +1,17 @@ +// src/routes/+page.server.ts + +import { prisma } from 'sil.appbuilder.portal.common'; +import type { PageServerLoad } from './$types'; + +export const load = (async () => { + const productDefinitions = await prisma.productDefinitions.findMany({ + include: { + ApplicationTypes: true, + Workflow: true, + RebuildWorkflow: true, + RepublishWorkflow: true + } + }); + + return { productDefinitions }; +}) satisfies PageServerLoad; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/+page.svelte new file mode 100644 index 0000000000..f031247625 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/+page.svelte @@ -0,0 +1,51 @@ + + + + {m.admin_settings_productDefinitions_add()} + + +
+ {#each data.productDefinitions.sort((a, b) => sortByName(a, b, languageTag())) as pD} + goto(base + '/admin/settings/product-definitions/edit?id=' + pD.Id)} + title={pD.Name} + fields={[ + { + key: 'admin_settings_productDefinitions_type', + value: pD.ApplicationTypes.Name // TODO: this doesn't actually mean anything for the product definition, so we may want to remove this entirely from the UI. + }, + { + key: 'admin_settings_productDefinitions_workflow', + value: pD.Workflow.Name + }, + { + key: 'admin_settings_productDefinitions_rebuildWorkflow', + value: pD.RebuildWorkflow?.Name + }, + { + key: 'admin_settings_productDefinitions_republishWorkflow', + value: pD.RepublishWorkflow?.Name + }, + { + key: 'admin_settings_productDefinitions_description', + value: pD.Description + } + ]} + /> + {/each} +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/edit/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/edit/+page.server.ts new file mode 100644 index 0000000000..bcec764355 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/edit/+page.server.ts @@ -0,0 +1,96 @@ +import { base } from '$app/paths'; +import { idSchema } from '$lib/valibot'; +import { fail, redirect } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const editSchema = v.object({ + id: idSchema, + name: v.nullable(v.string()), + applicationType: idSchema, + workflow: idSchema, + rebuildWorkflow: v.nullable(idSchema), + republishWorkflow: v.nullable(idSchema), + description: v.nullable(v.string()), + properties: v.nullable(v.string()) +}); +export const load = (async ({ url }) => { + const id = parseInt(url.searchParams.get('id') ?? ''); + if (isNaN(id)) { + return redirect(302, base + '/admin/settings/product-definitions'); + } + const options = { + applicationTypes: await prisma.applicationTypes.findMany(), + workflows: await prisma.workflowDefinitions.findMany() + }; + const data = await prisma.productDefinitions.findFirst({ + where: { + Id: id + } + }); + if (!data) return redirect(302, base + '/admin/settings/product-definitions'); + const form = await superValidate( + { + id: data.Id, + name: data.Name, + applicationType: data.TypeId, + workflow: data.WorkflowId, + rebuildWorkflow: data.RebuildWorkflowId, + republishWorkflow: data.RepublishWorkflowId, + description: data.Description, + properties: data.Properties + }, + valibot(editSchema) + ); + return { form, options }; +}) satisfies PageServerLoad; + +export const actions = { + async edit({ cookies, request }) { + const form = await superValidate(request, valibot(editSchema)); + if (!form.valid) { + return fail(400, { form, ok: false, errors: form.errors }); + } + try { + const { + id, + name, + applicationType, + workflow, + rebuildWorkflow, + republishWorkflow, + description, + properties + } = form.data; + await DatabaseWrites.productDefinitions.update({ + where: { + Id: id + }, + data: { + TypeId: applicationType, + Name: name, + WorkflowId: workflow, + RebuildWorkflowId: rebuildWorkflow, + RepublishWorkflowId: republishWorkflow, + Description: description, + Properties: properties + } + }); + return { ok: true, form }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + // const Id = data.get('id'); + // const name = data.get('name'); + // const applicationType = data.get('applicationType'); + // const workflow = data.get('workflow'); + // const rebuildWorkflow = data.get('rebuildWorkflow'); + // const republishWorkflow = data.get('republishWorkflow'); + // const description = data.get('description'); + // const properties = data.get('properties'); + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/edit/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/edit/+page.svelte new file mode 100644 index 0000000000..51a2e9c01d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/edit/+page.svelte @@ -0,0 +1,110 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + {#if $allErrors.length} +
    + {#each $allErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} +
+ + Cancel +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/new/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/new/+page.server.ts new file mode 100644 index 0000000000..3ffb768809 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/new/+page.server.ts @@ -0,0 +1,60 @@ +import { fail } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const createSchema = v.object({ + name: v.nullable(v.string()), + applicationType: v.pipe(v.number(), v.minValue(1), v.integer()), + workflow: v.pipe(v.number(), v.minValue(1), v.integer()), + rebuildWorkflow: v.nullable(v.pipe(v.number(), v.minValue(1), v.integer())), + republishWorkflow: v.nullable(v.pipe(v.number(), v.minValue(1), v.integer())), + description: v.nullable(v.string()), + properties: v.nullable(v.string()) +}); + +export const load = (async ({ url }) => { + const form = await superValidate(valibot(createSchema)); + const options = { + applicationTypes: await prisma.applicationTypes.findMany(), + workflows: await prisma.workflowDefinitions.findMany() + }; + return { form, options }; +}) satisfies PageServerLoad; + +export const actions = { + async new({ cookies, request }) { + const form = await superValidate(request, valibot(createSchema)); + if (!form.valid) { + return fail(400, { form, ok: false, errors: form.errors }); + } + try { + const { + name, + applicationType, + workflow, + rebuildWorkflow, + republishWorkflow, + description, + properties + } = form.data; + await DatabaseWrites.productDefinitions.create({ + data: { + Name: name, + TypeId: applicationType, + WorkflowId: workflow, + RebuildWorkflowId: rebuildWorkflow, + RepublishWorkflowId: republishWorkflow, + Description: description, + Properties: properties + } + }); + return { ok: true, form }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/new/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/new/+page.svelte new file mode 100644 index 0000000000..86e963539c --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/product-definitions/new/+page.svelte @@ -0,0 +1,101 @@ + + +

{m.admin_settings_productDefinitions_add()}

+ +
+ + + + + + + + + + + + + + + + + + + + + + {#if $allErrors.length} +
    + {#each $allErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} +
+ + Cancel +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/+layout.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/+layout.svelte new file mode 100644 index 0000000000..d4b2733b05 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/+layout.svelte @@ -0,0 +1,8 @@ + + +

{m.admin_settings_storeTypes_title()}

+{@render children?.()} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/+page.server.ts new file mode 100644 index 0000000000..b977f4f587 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/+page.server.ts @@ -0,0 +1,10 @@ +// src/routes/admin/settings/store-types/+page.server.ts + +import { prisma } from 'sil.appbuilder.portal.common'; +import type { PageServerLoad } from './$types'; + +export const load = (async () => { + const storeTypes = await prisma.storeTypes.findMany(); + + return { storeTypes }; +}) satisfies PageServerLoad; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/+page.svelte new file mode 100644 index 0000000000..5cc448cfd3 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/+page.svelte @@ -0,0 +1,29 @@ + + + + {m.admin_settings_storeTypes_add()} + + +
+ {#each data.storeTypes.sort((a, b) => sortByName(a, b, languageTag())) as storeType} + goto('/admin/settings/store-types/edit?id=' + storeType.Id)} + title={storeType.Name} + fields={[{ key: 'stores_attributes_description', value: storeType.Description }]} + /> + {/each} +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/edit/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/edit/+page.server.ts new file mode 100644 index 0000000000..a50218c795 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/edit/+page.server.ts @@ -0,0 +1,60 @@ +import { base } from '$app/paths'; +import { idSchema } from '$lib/valibot'; +import { fail, redirect } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const editSchema = v.object({ + id: idSchema, + name: v.nullable(v.string()), + description: v.nullable(v.string()) +}); +export const load = (async ({ url }) => { + const id = parseInt(url.searchParams.get('id') ?? ''); + if (isNaN(id)) { + return redirect(302, base + '/admin/settings/store-types'); + } + const data = await prisma.storeTypes.findFirst({ + where: { + Id: id + } + }); + if (!data) return redirect(302, base + '/admin/settings/store-types'); + const form = await superValidate( + { + id: data.Id, + name: data.Name, + description: data.Description + }, + valibot(editSchema) + ); + return { form }; +}) satisfies PageServerLoad; + +export const actions = { + async edit({ cookies, request }) { + const form = await superValidate(request, valibot(editSchema)); + if (!form.valid) { + return fail(400, { form, ok: false, errors: form.errors }); + } + try { + const { id, name, description } = form.data; + await DatabaseWrites.storeTypes.update({ + where: { + Id: id + }, + data: { + Name: name, + Description: description + } + }); + return { ok: true, form }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/edit/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/edit/+page.svelte new file mode 100644 index 0000000000..dac26f4ba3 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/edit/+page.svelte @@ -0,0 +1,57 @@ + + + +
+ + + + + + + + {#if $allErrors.length} +
    + {#each $allErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} +
+ + Cancel +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/new/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/new/+page.server.ts new file mode 100644 index 0000000000..512f9b9205 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/new/+page.server.ts @@ -0,0 +1,38 @@ +import { fail } from '@sveltejs/kit'; +import { DatabaseWrites } from 'sil.appbuilder.portal.common'; +import { superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const createSchema = v.object({ + name: v.nullable(v.string()), + description: v.nullable(v.string()) +}); + +export const load = (async ({ url }) => { + const form = await superValidate(valibot(createSchema)); + return { form }; +}) satisfies PageServerLoad; + +export const actions = { + async new({ cookies, request }) { + const form = await superValidate(request, valibot(createSchema)); + if (!form.valid) { + return fail(400, { form, ok: false, errors: form.errors }); + } + try { + const { name, description } = form.data; + await DatabaseWrites.storeTypes.create({ + data: { + Name: name, + Description: description + } + }); + return { ok: true, form }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/new/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/new/+page.svelte new file mode 100644 index 0000000000..5116c5c20b --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/store-types/new/+page.svelte @@ -0,0 +1,51 @@ + + +

{m.models_add({ name: m.admin_settings_storeTypes_add() })}

+ +
+ + + + + + + {#if $allErrors.length} +
    + {#each $allErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} +
+ + Cancel +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/+layout.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/+layout.svelte new file mode 100644 index 0000000000..ec128168dd --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/+layout.svelte @@ -0,0 +1,10 @@ + + +

{m.admin_settings_navigation_stores()}

+{@render children?.()} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/+page.server.ts new file mode 100644 index 0000000000..08ed82189d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/+page.server.ts @@ -0,0 +1,12 @@ +// src/routes/admin/settings/stores/+page.server.ts + +import { prisma } from 'sil.appbuilder.portal.common'; +import type { PageServerLoad } from './$types'; + +export const load = (async () => { + const stores = await prisma.stores.findMany({ + include: { StoreType: true } + }); + + return { stores }; +}) satisfies PageServerLoad; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/+page.svelte new file mode 100644 index 0000000000..3748620e4d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/+page.svelte @@ -0,0 +1,32 @@ + + + + {m.models_add({ name: m.stores_name() })} + + +
+ {#each data.stores.sort((a, b) => sortByName(a, b, languageTag())) as store} + goto('/admin/settings/stores/edit?id=' + store.Id)} + title={store.Name} + fields={[ + { key: 'stores_attributes_description', value: store.Description }, + { key: 'storeTypes_name', value: store.StoreType.Name } + ]} + /> + {/each} +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/edit/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/edit/+page.server.ts new file mode 100644 index 0000000000..5d36b89a6f --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/edit/+page.server.ts @@ -0,0 +1,64 @@ +import { base } from '$app/paths'; +import { idSchema } from '$lib/valibot'; +import { fail, redirect } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const editSchema = v.object({ + id: idSchema, + name: v.nullable(v.string()), + description: v.nullable(v.string()), + storeType: idSchema +}); +export const load = (async ({ url }) => { + const id = parseInt(url.searchParams.get('id') ?? ''); + if (isNaN(id)) { + return redirect(302, base + '/admin/settings/stores'); + } + const data = await prisma.stores.findFirst({ + where: { + Id: id + } + }); + if (!data) return redirect(302, base + '/admin/settings/stores'); + const options = await prisma.storeTypes.findMany(); + const form = await superValidate( + { + id: data.Id, + name: data.Name, + storeType: data.StoreTypeId!, + description: data.Description + }, + valibot(editSchema) + ); + return { form, options }; +}) satisfies PageServerLoad; + +export const actions = { + async edit({ cookies, request }) { + const form = await superValidate(request, valibot(editSchema)); + if (!form.valid) { + return fail(400, { form, ok: false, errors: form.errors }); + } + try { + const { id, name, description, storeType } = form.data; + await DatabaseWrites.stores.update({ + where: { + Id: id + }, + data: { + Name: name, + StoreTypeId: storeType, + Description: description + } + }); + return { ok: true, form }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/edit/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/edit/+page.svelte new file mode 100644 index 0000000000..3350101639 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/edit/+page.svelte @@ -0,0 +1,64 @@ + + + +
+ + + + + + + + + + + {#if $allErrors.length} +
    + {#each $allErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} +
+ + Cancel +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/new/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/new/+page.server.ts new file mode 100644 index 0000000000..56d61d38a1 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/new/+page.server.ts @@ -0,0 +1,43 @@ +import { fail } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const createSchema = v.object({ + name: v.nullable(v.string()), + storeType: v.pipe(v.number(), v.minValue(1), v.integer()), + description: v.nullable(v.string()) +}); + +export const load = (async ({ url }) => { + const form = await superValidate(valibot(createSchema)); + const options = { + storeType: await prisma.storeTypes.findMany() + }; + return { form, options }; +}) satisfies PageServerLoad; + +export const actions = { + async new({ cookies, request }) { + const form = await superValidate(request, valibot(createSchema)); + if (!form.valid) { + return fail(400, { form, ok: false, errors: form.errors }); + } + try { + const { name, description, storeType } = form.data; + await DatabaseWrites.stores.create({ + data: { + Name: name, + Description: description, + StoreTypeId: storeType + } + }); + return { ok: true, form }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/new/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/new/+page.svelte new file mode 100644 index 0000000000..d5d8958162 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/stores/new/+page.svelte @@ -0,0 +1,59 @@ + + +

{m.models_add({ name: m.stores_name() })}

+ +
+ + + + + + + + + + {#if $allErrors.length} +
    + {#each $allErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} + +
+ + Cancel +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/+layout.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/+layout.svelte new file mode 100644 index 0000000000..11f883d40b --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/+layout.svelte @@ -0,0 +1,10 @@ + + +

{m.admin_settings_workflowDefinitions_title()}

+{@render children?.()} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/+page.server.ts new file mode 100644 index 0000000000..5c02001a01 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/+page.server.ts @@ -0,0 +1,12 @@ +// src/routes/+page.server.ts + +import { prisma } from 'sil.appbuilder.portal.common'; +import type { PageServerLoad } from './$types'; + +export const load = (async () => { + const workflowDefinitions = await prisma.workflowDefinitions.findMany({ + include: { StoreType: true } + }); + + return { workflowDefinitions }; +}) satisfies PageServerLoad; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/+page.svelte new file mode 100644 index 0000000000..d3e329b130 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/+page.svelte @@ -0,0 +1,57 @@ + + + + {m.admin_settings_workflowDefinitions_add()} + + +
+ {#each data.workflowDefinitions.sort((a, b) => sortByName(a, b, languageTag())) as wd} + goto('/admin/settings/workflow-definitions/edit?id=' + wd.Id)} + title={wd.Name} + fields={[ + { + key: 'admin_settings_workflowDefinitions_description', + value: wd.Description + }, + { + key: 'admin_settings_workflowDefinitions_storeType', + value: wd.StoreType?.Name + }, + { + key: 'admin_settings_workflowDefinitions_productType', + value: [ + 'Android GooglePlay', + 'Android S3', + m.admin_settings_workflowDefinitions_productType_assetPackage(), + m.admin_settings_workflowDefinitions_productType_web() + ][wd.ProductType] + }, + { + key: 'admin_settings_workflowDefinitions_workflowType', + value: [ + , + m.admin_settings_workflowDefinitions_workflowTypes_1(), + m.admin_settings_workflowDefinitions_workflowTypes_2(), + m.admin_settings_workflowDefinitions_workflowTypes_3() + ][wd.Type] + }, + // Do we want to show options here? + ]} + /> + {/each} +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/common.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/common.ts new file mode 100644 index 0000000000..e11dc82c63 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/common.ts @@ -0,0 +1,23 @@ +import { idSchema } from '$lib/valibot'; +import * as v from 'valibot'; +import { ProductType, WorkflowOptions } from 'sil.appbuilder.portal.common/workflow'; + +export const businessFlows = [ + 'SIL_AppBuilders_AssetPackage_Flow', + 'SIL_AppBuilders_Web_Flow', + 'SIL_Default_AppBuilders_Android_GooglePlay_Flow', + 'SIL_Default_AppBuilders_Android_S3_Flow' +]; + +export const workflowDefinitionSchemaBase = v.object({ + name: v.nullable(v.string()), + storeType: idSchema, + productType: v.pipe(idSchema, v.enum(ProductType)), + workflowType: idSchema, + workflowScheme: v.nullable(v.string()), + workflowBusinessFlow: v.nullable(v.picklist(businessFlows)), + description: v.nullable(v.string()), + properties: v.nullable(v.string()), + options: v.array(v.pipe(idSchema, v.enum(WorkflowOptions))), + enabled: v.boolean() +}); diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/edit/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/edit/+page.server.ts new file mode 100644 index 0000000000..fbd712b3d8 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/edit/+page.server.ts @@ -0,0 +1,90 @@ +import { base } from '$app/paths'; +import { idSchema } from '$lib/valibot'; +import { fail, redirect } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; +import { workflowDefinitionSchemaBase } from '../common'; + +const editSchema = v.object({ + id: idSchema, + ...workflowDefinitionSchemaBase.entries +}); +export const load = (async ({ url }) => { + const id = parseInt(url.searchParams.get('id') ?? ''); + if (isNaN(id)) { + return redirect(302, base + '/admin/settings/workflow-definitions'); + } + const data = await prisma.workflowDefinitions.findFirst({ + where: { + Id: id + } + }); + if (!data) return redirect(302, base + '/admin/settings/workflow-definitions'); + const storeTypes = await prisma.storeTypes.findMany(); + const schemes = await prisma.workflowScheme.findMany({ select: { Code: true }}); + const form = await superValidate( + { + id: data.Id, + name: data.Name, + storeType: data.StoreTypeId!, + productType: data.ProductType, + workflowType: data.Type, + workflowScheme: data.WorkflowScheme, + workflowBusinessFlow: data.WorkflowBusinessFlow, + description: data.Description, + properties: data.Properties, + options: data.WorkflowOptions, + enabled: data.Enabled + }, + valibot(editSchema) + ); + return { form, storeTypes, schemes }; +}) satisfies PageServerLoad; + +export const actions = { + async edit({ cookies, request }) { + const form = await superValidate(request, valibot(editSchema)); + if (!form.valid) { + return fail(400, { form, ok: false, errors: form.errors }); + } + try { + const { + id, + name, + description, + properties, + enabled, + storeType, + workflowBusinessFlow, + workflowScheme, + workflowType, + options, + productType + } = form.data; + await DatabaseWrites.workflowDefinitions.update({ + where: { + Id: id + }, + data: { + Type: workflowType, + Name: name, + WorkflowScheme: workflowScheme, + WorkflowBusinessFlow: workflowBusinessFlow, + StoreTypeId: storeType, + Description: description, + Properties: properties, + Enabled: enabled, + ProductType: productType, + WorkflowOptions: options + } + }); + return { ok: true, form }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/edit/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/edit/+page.svelte new file mode 100644 index 0000000000..2444b4d4b0 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/edit/+page.svelte @@ -0,0 +1,186 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + {#each workflowOptions as opt} +
+
+ + {opt.message} + +
+ +
+ {/each} +
+
+ +
+ {#if $allErrors.length} +
    + {#each $allErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} + +
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/new/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/new/+page.server.ts new file mode 100644 index 0000000000..03f289234d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/new/+page.server.ts @@ -0,0 +1,60 @@ +import { fail } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; +import { workflowDefinitionSchemaBase } from '../common'; + +const createSchema = workflowDefinitionSchemaBase; + +export const load = (async ({ url }) => { + const form = await superValidate(valibot(createSchema)); + const options = { + storeType: await prisma.storeTypes.findMany(), + schemes: await prisma.workflowScheme.findMany({ select: { Code: true }}) + }; + + return { form, options }; +}) satisfies PageServerLoad; + +export const actions = { + async new({ cookies, request }) { + const form = await superValidate(request, valibot(createSchema)); + if (!form.valid) { + return fail(400, { form, ok: false, errors: form.errors }); + } + try { + const { + name, + description, + properties, + enabled, + storeType, + workflowBusinessFlow, + workflowScheme, + workflowType, + productType, + options + } = form.data; + await DatabaseWrites.workflowDefinitions.create({ + data: { + Name: name, + Description: description, + Properties: properties, + Enabled: enabled, + StoreTypeId: storeType, + WorkflowBusinessFlow: workflowBusinessFlow, + WorkflowScheme: workflowScheme, + Type: workflowType, + ProductType: productType, + WorkflowOptions: options + } + }); + return { ok: true, form }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/new/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/new/+page.svelte new file mode 100644 index 0000000000..145ab6eee3 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/new/+page.svelte @@ -0,0 +1,168 @@ + + +

{m.admin_settings_workflowDefinitions_add()}

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + {#each workflowOptions as opt} +
+
+ + {opt.message} + +
+ +
+ {/each} +
+
+ +
+ {#if $allErrors.length} +
    + {#each $allErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} + +
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/directory/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/directory/+page.server.ts new file mode 100644 index 0000000000..4b13eaa432 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/directory/+page.server.ts @@ -0,0 +1,91 @@ +import { projectSearchSchema, pruneProjects } from '$lib/projects/common'; +import { projectFilter } from '$lib/projects/common.server'; +import type { Prisma } from '@prisma/client'; +import { error, type Actions } from '@sveltejs/kit'; +import { prisma } from 'sil.appbuilder.portal.common'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import type { PageServerLoad } from './$types'; + +export const load = (async ({ locals }) => { + const userId = (await locals.auth())?.user.userId; + if (!userId) return error(400); + const projects = await prisma.projects.findMany({ + where: { + IsPublic: true + }, + include: { + Products: { + include: { + ProductDefinition: true, + WorkflowInstance: true + } + }, + Owner: true, + Group: true, + Organization: true + }, + take: 10 + }); + const productDefinitions = await prisma.productDefinitions.findMany(); + return { + projects: pruneProjects(projects), + productDefinitions, + form: await superValidate( + { + page: { + page: 0, + size: 10 + } + }, + valibot(projectSearchSchema) + ), + count: await prisma.projects.count({ + where: { + IsPublic: true + } + }), + organizations: await prisma.organizations.findMany({ + select: { + Id: true, + Name: true + } + }) + }; +}) satisfies PageServerLoad; + +export const actions: Actions = { + page: async function ({ request, locals }) { + const userId = (await locals.auth())?.user.userId; + if (!userId) return error(400); + + const form = await superValidate(request, valibot(projectSearchSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + + const where: Prisma.ProjectsWhereInput = { + ...projectFilter(form.data), + IsPublic: true + }; + + const projects = await prisma.projects.findMany({ + where: where, + include: { + Products: { + include: { + ProductDefinition: true, + WorkflowInstance: true + } + }, + Owner: true, + Group: true, + Organization: true + }, + skip: form.data.page.size * form.data.page.page, + take: form.data.page.size + }); + + const count = await prisma.projects.count({ where: where }); + + return { form, ok: true, query: { data: pruneProjects(projects), count } }; + } +}; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/directory/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/directory/+page.svelte new file mode 100644 index 0000000000..78f5b9bf16 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/directory/+page.svelte @@ -0,0 +1,137 @@ + + +
+ +
{ + if (event.key === 'Enter') submit(); + }} + > +
+
+

{m.sidebar_projectDirectory()}

+
+
+ + +
+
+
+
+ submit()} + inputClasses="w-full max-w-xs" + /> +
+ + +
+
+ {#if projects.length > 0} +
+ {#each projects as project} + + {/each} +
+ {:else} +

{m.projectTable_empty()}

+ {/if} + +
{ + if (event.key === 'Enter') submit(); + }} + > +
+ +
+
+
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/open-source/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/open-source/+page.svelte new file mode 100644 index 0000000000..55fea15226 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/open-source/+page.svelte @@ -0,0 +1,25 @@ + + +
+

{m.attributions_title()}

+

{m.attributions_subtitle()}

+ +
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/+page.server.ts new file mode 100644 index 0000000000..71f69f61c0 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/+page.server.ts @@ -0,0 +1,30 @@ +import { isSuperAdmin } from '$lib/utils'; +import { redirect } from '@sveltejs/kit'; +import { prisma } from 'sil.appbuilder.portal.common'; +import type { PageServerLoad } from './$types'; + +export const load = (async (event) => { + const user = (await event.locals.auth())!.user; + const organizations = isSuperAdmin(user.roles) + ? await prisma.organizations.findMany({ + include: { + Owner: true + } + }) + : await prisma.organizations.findMany({ + where: { + OrganizationMemberships: { + every: { + UserId: user.userId + } + } + }, + include: { + Owner: true + } + }); + if (organizations.length === 1) { + return redirect(302, '/organizations/' + organizations[0].Id + '/settings'); + } + return { organizations }; +}) satisfies PageServerLoad; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/+page.svelte new file mode 100644 index 0000000000..51fac2d4b1 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/+page.svelte @@ -0,0 +1,21 @@ + + +
+

{m.org_settingsTitle()}

+ goto(page.url.pathname + '/' + id + '/settings/info')} + /> +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/+layout.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/+layout.server.ts new file mode 100644 index 0000000000..58bb80ed41 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/+layout.server.ts @@ -0,0 +1,15 @@ +import { redirect } from '@sveltejs/kit'; +import { prisma } from 'sil.appbuilder.portal.common'; +import type { LayoutServerLoad } from './$types'; + +export const load = (async (event) => { + const id = parseInt(event.params.id); + if (isNaN(id)) return redirect(302, '/organizations'); + const organization = await prisma.organizations.findUnique({ + where: { + Id: id + } + }); + if (!organization) return redirect(302, '/organizations'); + return { organization }; +}) satisfies LayoutServerLoad; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/+layout.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/+layout.svelte new file mode 100644 index 0000000000..32ea754bd9 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/+layout.svelte @@ -0,0 +1,58 @@ + + + + {#snippet title()} +
+

+ {org_settingsTitle()} +

+

{data.organization.Name}

+
+ {/snippet} + {@render children?.()} +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/+page.svelte new file mode 100644 index 0000000000..e69de29bb2 diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/groups/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/groups/+page.server.ts new file mode 100644 index 0000000000..5275a8fe50 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/groups/+page.server.ts @@ -0,0 +1,58 @@ +import { idSchema } from '$lib/valibot'; +import { redirect } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const addGroupSchema = v.object({ + id: idSchema, + name: v.string(), + abbreviation: v.string() +}); +const deleteGroupSchema = v.object({ + id: idSchema +}); + +export const load = (async (event) => { + if (isNaN(parseInt(event.params.id))) return redirect(302, '/organizations'); + const organization = await prisma.organizations.findUnique({ + where: { + Id: parseInt(event.params.id) + }, + include: { + Groups: true + } + }); + if (!organization) return redirect(302, '/organizations'); + const addForm = await superValidate(valibot(addGroupSchema)); + const deleteForm = await superValidate(valibot(deleteGroupSchema)); + return { organization, addForm, deleteForm }; +}) satisfies PageServerLoad; + +export const actions = { + async addGroup(event) { + const form = await superValidate(event.request, valibot(addGroupSchema)); + if (!form.valid) return fail(400, { form, ok: false, errors: form.errors }); + try { + const { id, name, abbreviation } = form.data; + await DatabaseWrites.groups.createGroup(name, abbreviation, id); + return { form, ok: true }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + }, + async deleteGroup(event) { + const form = await superValidate(event.request, valibot(deleteGroupSchema)); + if (!form.valid) return fail(400, { form, ok: false, errors: form.errors }); + try { + const { id } = form.data; + return { form, ok: await DatabaseWrites.groups.deleteGroup(id) }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/groups/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/groups/+page.svelte new file mode 100644 index 0000000000..7284b19abb --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/groups/+page.svelte @@ -0,0 +1,44 @@ + + +{#each data.organization.Groups as group} +
+ +
+
+ {group.Abbreviation} + {group.Name} +
+ +
+
+{/each} +
+ {org_addGroupButton()} + +
+ + + +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/info/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/info/+page.server.ts new file mode 100644 index 0000000000..0ed93fbcf2 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/info/+page.server.ts @@ -0,0 +1,56 @@ +import { idSchema } from '$lib/valibot'; +import { redirect } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const editInfoSchema = v.object({ + id: idSchema, + name: v.nullable(v.string()), + logoUrl: v.nullable(v.string()) +}); + +export const load = (async (event) => { + if (isNaN(parseInt(event.params.id))) return redirect(302, '/organizations'); + const organization = await prisma.organizations.findUnique({ + where: { + Id: parseInt(event.params.id) + } + }); + if (!organization) return redirect(302, '/organizations'); + const form = await superValidate( + { + id: organization.Id, + name: organization.Name, + logoUrl: organization.LogoUrl + }, + valibot(editInfoSchema) + ); + return { organization, form }; +}) satisfies PageServerLoad; + +export const actions = { + async default(event) { + const form = await superValidate(event.request, valibot(editInfoSchema)); + if (!form.valid) return fail(400, { form, ok: false, errors: form.errors }); + try { + const { id, name, logoUrl } = form.data; + await DatabaseWrites.organizations.update({ + where: { + Id: id + }, + data: { + Name: name, + LogoUrl: logoUrl + } + }); + return { form, ok: true }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + return { form, ok: true }; + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/info/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/info/+page.svelte new file mode 100644 index 0000000000..387015cfb5 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/info/+page.svelte @@ -0,0 +1,45 @@ + + +
+ +
+
+ + + + + + {org_noteLogUrl()} + +
+
+ Logo +
+
+
+ +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/infrastructure/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/infrastructure/+page.server.ts new file mode 100644 index 0000000000..a9100d551d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/infrastructure/+page.server.ts @@ -0,0 +1,58 @@ +import { idSchema } from '$lib/valibot'; +import { redirect } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const infrastructureSchema = v.object({ + id: idSchema, + buildEngineUrl: v.nullable(v.string()), + buildEngineApiAccessToken: v.nullable(v.string()), + useDefaultBuildEngine: v.boolean() +}); + +export const load = (async (event) => { + if (isNaN(parseInt(event.params.id))) return redirect(302, '/organizations'); + const organization = await prisma.organizations.findUnique({ + where: { + Id: parseInt(event.params.id) + } + }); + if (!organization) return redirect(302, '/organizations'); + const form = await superValidate( + { + id: organization.Id, + buildEngineUrl: organization.BuildEngineUrl, + buildEngineApiAccessToken: organization.BuildEngineApiAccessToken, + useDefaultBuildEngine: organization.UseDefaultBuildEngine ?? false + }, + valibot(infrastructureSchema) + ); + return { organization, form }; +}) satisfies PageServerLoad; + +export const actions = { + async default(event) { + const form = await superValidate(event.request, valibot(infrastructureSchema)); + if (!form.valid) return fail(400, { form, ok: false, errors: form.errors }); + try { + const { id, buildEngineApiAccessToken, buildEngineUrl, useDefaultBuildEngine } = form.data; + await DatabaseWrites.organizations.update({ + where: { + Id: id + }, + data: { + BuildEngineApiAccessToken: buildEngineApiAccessToken, + BuildEngineUrl: buildEngineUrl, + UseDefaultBuildEngine: useDefaultBuildEngine + } + }); + return { form, ok: true }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/infrastructure/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/infrastructure/+page.svelte new file mode 100644 index 0000000000..9a78cebaea --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/infrastructure/+page.svelte @@ -0,0 +1,57 @@ + + +
+ +
+
+
+ +
+ + + + + + +
+
+
+ +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/products/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/products/+page.server.ts new file mode 100644 index 0000000000..35761f7016 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/products/+page.server.ts @@ -0,0 +1,74 @@ +import { base } from '$app/paths'; +import { idSchema } from '$lib/valibot'; +import { redirect } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const editProductsSchema = v.object({ + id: idSchema, + publicByDefault: v.boolean(), + products: v.array( + v.object({ + productId: idSchema, + enabled: v.boolean() + }) + ) +}); +export const load = (async (event) => { + const id = parseInt(event.params.id); + if (isNaN(id)) return redirect(302, base + '/organizations'); + const data = await prisma.organizations.findUnique({ + where: { + Id: id + } + }); + const orgProductDefs = await prisma.organizationProductDefinitions.findMany({ + where: { + OrganizationId: id + } + }); + const allProductDefs = await prisma.productDefinitions.findMany(); + if (!data) return redirect(302, base + '/organizations'); + const setOrgProductDefs = new Set(orgProductDefs.map((p) => p.ProductDefinitionId)); + const form = await superValidate( + { + id: data.Id, + publicByDefault: data.PublicByDefault ?? false, + products: allProductDefs.map((pD) => ({ + productId: pD.Id, + enabled: setOrgProductDefs.has(pD.Id) + })) + }, + valibot(editProductsSchema) + ); + return { organization: data, orgProductDefs, allProductDefs, form }; +}) satisfies PageServerLoad; + +export const actions = { + async default(event) { + const form = await superValidate(event.request, valibot(editProductsSchema)); + if (!form.valid) return fail(400, { form, ok: false, errors: form.errors }); + try { + const { id, publicByDefault, products } = form.data; + await DatabaseWrites.organizationProductDefinitions.updateOrganizationProductDefinitions( + id, + products.filter((p) => p.enabled).map((p) => p.productId) + ); + await DatabaseWrites.organizations.update({ + where: { + Id: id + }, + data: { + PublicByDefault: publicByDefault + } + }); + return { ok: true, form }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/products/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/products/+page.svelte new file mode 100644 index 0000000000..3773d41f4f --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/products/+page.svelte @@ -0,0 +1,58 @@ + + +

{m.org_productsTitle()}

+
+
+ +
+ + {#each $superFormData.products as productDef} + p.Id === productDef.productId)?.Name ?? ''} + description={data.allProductDefs.find((p) => p.Id === productDef.productId)?.Description ?? + ''} + bind:checked={productDef.enabled} + /> + {/each} + +
+ +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/stores/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/stores/+page.server.ts new file mode 100644 index 0000000000..0203347524 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/stores/+page.server.ts @@ -0,0 +1,64 @@ +import { base } from '$app/paths'; +import { idSchema } from '$lib/valibot'; +import { redirect } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const editStoresSchema = v.object({ + id: idSchema, + stores: v.array( + v.object({ + storeId: idSchema, + enabled: v.boolean() + }) + ) +}); +export const load = (async (event) => { + const id = parseInt(event.params.id); + if (isNaN(id)) return redirect(302, base + '/organizations'); + const data = await prisma.organizations.findUnique({ + where: { + Id: id + } + }); + const orgStores = await prisma.organizationStores.findMany({ + where: { + OrganizationId: id + } + }); + const allStores = await prisma.stores.findMany(); + if (!data) return redirect(302, base + '/organizations'); + const setOrgProductDefs = new Set(orgStores.map((p) => p.StoreId)); + const form = await superValidate( + { + id: data.Id, + stores: allStores.map((pD) => ({ + storeId: pD.Id, + enabled: setOrgProductDefs.has(pD.Id) + })) + }, + valibot(editStoresSchema) + ); + return { organization: data, orgStores, allStores, form }; +}) satisfies PageServerLoad; + +export const actions = { + async default(event) { + const form = await superValidate(event.request, valibot(editStoresSchema)); + if (!form.valid) return fail(400, { form, ok: false, errors: form.errors }); + try { + const { id, stores } = form.data; + await DatabaseWrites.organizationStores.updateOrganizationStores( + id, + stores.filter((s) => s.enabled).map((s) => s.storeId) + ); + return { ok: true, form }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/stores/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/stores/+page.svelte new file mode 100644 index 0000000000..341ad15a61 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/organizations/[id]/settings/stores/+page.svelte @@ -0,0 +1,39 @@ + + +

{m.org_storesTitle()}

+
+ +
+ {#each $superFormData.stores as store} + p.Id === store.storeId)?.Name ?? ''} + description={data.allStores.find((p) => p.Id === store.storeId)?.Description ?? ''} + bind:checked={store.enabled} + /> + {/each} +
+
+
+ +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/products/[id]/files/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/products/[id]/files/+page.server.ts new file mode 100644 index 0000000000..f029d73df6 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/products/[id]/files/+page.server.ts @@ -0,0 +1,129 @@ +import { paginateSchema } from '$lib/table'; +import { error, fail } from '@sveltejs/kit'; +import { prisma } from 'sil.appbuilder.portal.common'; +import { superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import type { Actions, PageServerLoad } from './$types'; + +export const load = (async ({ params }) => { + const builds = await prisma.productBuilds.findMany({ + orderBy: [ + { + DateUpdated: 'desc' + } + ], + where: { + ProductId: params.id + }, + select: { + Id: true, + Version: true, + BuildId: true, + Success: true, + ProductArtifacts: { + select: { + ArtifactType: true, + Url: true, + FileSize: true, + DateUpdated: true + } + }, + ProductPublications: { + select: { + Channel: true, + Success: true, + DateUpdated: true, + LogUrl: true + }, + orderBy: { + DateUpdated: 'desc' + }, + take: 1 + } + }, + take: 3 + }); + const product = await prisma.products.findUnique({ + where: { + Id: params.id + }, + select: { + WorkflowBuildId: true, + ProductDefinition: { + select: { + Name: true + } + }, + Project: { + select: { + Id: true, + Name: true + } + } + } + }); + return { + product, + builds, + form: await superValidate({ page: 0, size: 3 }, valibot(paginateSchema)), + count: await prisma.productBuilds.count({ where: { ProductId: params.id } }) + }; +}) satisfies PageServerLoad; + +export const actions = { + page: async ({ request, params, locals }) => { + const session = await locals.auth(); + if (!session) return error(403); + const form = await superValidate(request, valibot(paginateSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + + const builds = await prisma.productBuilds.findMany({ + orderBy: [ + { + DateUpdated: 'desc' + } + ], + where: { + ProductId: params.id + }, + select: { + Id: true, + Version: true, + BuildId: true, + Success: true, + ProductArtifacts: { + select: { + ArtifactType: true, + Url: true, + FileSize: true, + DateUpdated: true + } + }, + ProductPublications: { + select: { + Channel: true, + Success: true, + DateUpdated: true, + LogUrl: true + }, + orderBy: { + DateUpdated: 'desc' + }, + take: 1 + } + }, + skip: form.data.page * form.data.size, + take: form.data.size + }); + + return { + form, + ok: true, + query: { + data: builds, + // update count, just in case more builds were added + count: await prisma.productBuilds.count({ where: { ProductId: params.id } }) + } + }; + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/products/[id]/files/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/products/[id]/files/+page.svelte new file mode 100644 index 0000000000..63039fe281 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/products/[id]/files/+page.svelte @@ -0,0 +1,71 @@ + + +
+
+ +

{m.products_files_title()}

+
+
+ {#each builds as build} + + {/each} +
+
+ + +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/products/[id]/files/components/BuildArtifacts.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/products/[id]/files/components/BuildArtifacts.svelte new file mode 100644 index 0000000000..5856698541 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/products/[id]/files/components/BuildArtifacts.svelte @@ -0,0 +1,148 @@ + + +
+
+ + {versionString(build)} + + + {pluralized(build.ProductArtifacts.length)} + +
+
+ {#if !build.ProductArtifacts.length} + {m.project_products_noArtifacts()} + {:else} + {@const langTag = languageTag()} + + + + + + + + + + + {#each build.ProductArtifacts as artifact} + + + + + + + {/each} + +
{m.project_products_filename()}{m.project_products_updated()}{m.project_products_size()}
{artifact.ArtifactType} + + {getRelativeTime(artifact.DateUpdated)} + + {bytesToHumanSize(artifact.FileSize)} + + + +
+ {/if} + {#if build.ProductPublications.at(0)?.LogUrl} + {@const pub = build.ProductPublications[0]} + + + + + + + + + + + + + + + + + + +
{m.project_products_publications_channel()}{m.project_products_publications_status()}{m.project_products_publications_date()}{m.project_products_publications_url()}
{pub.Channel} + {pub.Success + ? m.project_products_publications_succeeded() + : m.project_products_publications_failed()} + {pub.DateUpdated?.toLocaleDateString(languageTag())} + {m.project_products_publications_console()} +
+ {/if} +
+
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[filter=projectSelector]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[filter=projectSelector]/+page.server.ts new file mode 100644 index 0000000000..c06d9ef025 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[filter=projectSelector]/+page.server.ts @@ -0,0 +1,9 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load = (async (event) => { + const data = await event.parent(); + if (data.organizations.length === 1) + return redirect(302, event.url.pathname + '/' + data.organizations[0].Id); + return {}; +}) satisfies PageServerLoad; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[filter=projectSelector]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[filter=projectSelector]/+page.svelte new file mode 100644 index 0000000000..4296afbce8 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[filter=projectSelector]/+page.svelte @@ -0,0 +1,17 @@ + + +
+
+ +
+ + goto(page.url.pathname + '/' + id)} + /> +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[filter=projectSelector]/[id]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[filter=projectSelector]/[id]/+page.server.ts new file mode 100644 index 0000000000..a75fc08493 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[filter=projectSelector]/[id]/+page.server.ts @@ -0,0 +1,196 @@ +import { ProductActionType } from '$lib/products'; +import { doProductAction } from '$lib/products/server'; +import { + bulkProjectActionSchema, + canModifyProject, + projectSearchSchema, + pruneProjects +} from '$lib/projects/common'; +import { doProjectAction, projectFilter, userGroupsForOrg } from '$lib/projects/common.server'; +import type { Prisma } from '@prisma/client'; +import { error, redirect, type Actions } from '@sveltejs/kit'; +import { prisma } from 'sil.appbuilder.portal.common'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { PageServerLoad } from './$types'; + +const bulkProductActionSchema = v.object({ + products: v.array(v.pipe(v.string(), v.uuid())), + operation: v.nullable(v.pipe(v.enum(ProductActionType), v.excludes(ProductActionType.Cancel))) +}); + +function whereStatements( + filter: string, + orgId: number, + userId?: number +): Prisma.ProjectsWhereInput { + const selector = filter as 'organization' | 'active' | 'archived' | 'all' | 'own'; + switch (selector) { + case 'organization': + return { + OrganizationId: orgId, + DateArchived: null + }; + case 'active': + return { + OrganizationId: orgId, + DateActive: { + not: null + }, + DateArchived: null + }; + case 'archived': + return { + OrganizationId: orgId, + DateArchived: { + not: null + } + }; + case 'all': + return { + OrganizationId: orgId + }; + case 'own': + return { + OrganizationId: orgId, + OwnerId: userId, + DateArchived: null + }; + } +} + +export const load = (async ({ params, url, locals }) => { + const userId = (await locals.auth())?.user.userId; + const orgId = parseInt(params.id); + if (isNaN(orgId) || !(orgId + '' === params.id)) { + const paths = url.pathname.split('/'); + return redirect(302, paths.slice(0, paths.length - 1).join('/')); + } + const projects = await prisma.projects.findMany({ + where: whereStatements(params.filter, orgId, userId), + include: { + Products: { + include: { + ProductDefinition: true, + WorkflowInstance: true + } + }, + Owner: true, + Group: true, + Organization: true + }, + take: 10 + }); + return { + projects: pruneProjects(projects), + pageForm: await superValidate( + { + page: { + page: 0, + size: 10 + }, + organizationId: orgId + }, + valibot(projectSearchSchema) + ), + count: await prisma.projects.count({ where: whereStatements(params.filter, orgId, userId) }), + actionForm: await superValidate(valibot(bulkProjectActionSchema)), + productForm: await superValidate(valibot(bulkProductActionSchema)), + productDefinitions: await prisma.productDefinitions.findMany(), + /** allow actions other than reactivation */ + allowActions: params.filter !== 'archived', + allowReactivate: params.filter === 'all' || params.filter === 'archived', + userGroups: (await userGroupsForOrg(userId!, orgId)).map((g) => g.GroupId) + }; +}) satisfies PageServerLoad; + +export const actions: Actions = { + page: async function ({ params, request, locals }) { + const userId = (await locals.auth())?.user.userId; + if (!userId) return error(400); + + const form = await superValidate(request, valibot(projectSearchSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + + const where: Prisma.ProjectsWhereInput = { + ...projectFilter(form.data), + ...whereStatements(params.filter!, parseInt(params.id!), userId) + }; + + const projects = await prisma.projects.findMany({ + where: where, + include: { + Products: { + include: { + ProductDefinition: true, + WorkflowInstance: true + } + }, + Owner: true, + Group: true, + Organization: true + }, + skip: form.data.page.size * form.data.page.page, + take: form.data.page.size + }); + + const count = await prisma.projects.count({ where: where }); + + return { form, ok: true, query: { data: pruneProjects(projects), count } }; + }, + projectAction: async (event) => { + const session = await event.locals.auth(); + if (!session) return fail(403); + const orgId = parseInt(event.params.id!); + if (isNaN(orgId) || !(orgId + '' === event.params.id)) return fail(404); + + const form = await superValidate(event.request, valibot(bulkProjectActionSchema)); + if ( + !form.valid || + !form.data.operation || + (!form.data.projects?.length && form.data.projectId === null) + ) + return fail(400, { form, ok: false }); + // prefer single project over array + const projects = await prisma.projects.findMany({ + where: { + Id: { in: form.data.projectId !== null ? [form.data.projectId] : form.data.projects } + }, + select: { + Id: true, + DateArchived: true, + OwnerId: true, + GroupId: true + } + }); + if (!projects.every((p) => canModifyProject(session, p.OwnerId, orgId))) { + return fail(403); + } + + const groups = + form.data.operation === 'claim' + ? (await userGroupsForOrg(session.user.userId, orgId)).map((g) => g.GroupId) + : []; + await Promise.all( + projects.map(async (project) => { + await doProjectAction(form.data.operation, project, session, orgId, groups); + }) + ); + + return { form, ok: true }; + }, + productAction: async (event) => { + const session = await event.locals.auth(); + if (!session) return fail(403); + const orgId = parseInt(event.params.id!); + if (isNaN(orgId) || !(orgId + '' === event.params.id)) return fail(404); + + const form = await superValidate(event.request, valibot(bulkProductActionSchema)); + if (!form.valid || !form.data.operation) return fail(400, { form, ok: false }); + + await Promise.all(form.data.products.map((p) => doProductAction(p, form.data.operation!))); + + return { form, ok: true }; + } +}; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[filter=projectSelector]/[id]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[filter=projectSelector]/[id]/+page.svelte new file mode 100644 index 0000000000..85e27e6e02 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[filter=projectSelector]/[id]/+page.svelte @@ -0,0 +1,376 @@ + + +
+ +
{ + if (event.key === 'Enter') pageSubmit(); + }} + > +
+
+ +
+
+ + +
+
+
+
+
+ + {#if data.allowActions && (canArchiveSelected || !selectedProjects.length)} + + {/if} + {#if data.allowReactivate && (canReactivateSelected || !selectedProjects.length)} + + {/if} + {#if data.allowActions && (canArchiveSelected || !selectedProjects.length)} + + {/if} +
+ + + + + {#if page.params.filter === 'own'} + + {/if} +
+ {#if projects.length > 0} +
+ {#each projects as project} + + {#snippet select()} + + {/snippet} + {#snippet actions()} + + {/snippet} + + {/each} +
+ {:else} +

{m.projectTable_empty()}

+ {/if} + +
{ + if (event.key === 'Enter') pageSubmit(); + }} + > +
+ +
+
+
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/+page.server.ts new file mode 100644 index 0000000000..8a51067aa0 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/+page.server.ts @@ -0,0 +1,479 @@ +import { getProductActions, ProductActionType } from '$lib/products'; +import { doProductAction } from '$lib/products/server'; +import { canModifyProject, projectActionSchema } from '$lib/projects/common'; +import { + doProjectAction, + userGroupsForOrg, + verifyCanViewAndEdit +} from '$lib/projects/common.server'; +import { idSchema } from '$lib/valibot'; +import { error } from '@sveltejs/kit'; +import { BullMQ, DatabaseWrites, prisma, Queues } from 'sil.appbuilder.portal.common'; +import { RoleId } from 'sil.appbuilder.portal.common/prisma'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const deleteReviewerSchema = v.object({ + id: idSchema +}); +const deleteAuthorSchema = v.object({ + id: idSchema +}); +const addAuthorSchema = v.object({ + author: idSchema +}); +const addReviewerSchema = v.object({ + name: v.string(), + email: v.pipe(v.string(), v.email()), + language: v.string() +}); +const updateOwnerGroupSchema = v.object({ + owner: idSchema, + group: idSchema +}); +const addProductSchema = v.object({ + productDefinitionId: idSchema, + storeId: idSchema +}); + +const productActionSchema = v.object({ + productId: v.pipe(v.string(), v.uuid()), + productAction: v.enum(ProductActionType) +}); + +// Are we sending too much data? +// Maybe? I pared it down a bit with `select` instead of `include` - Aidan +export const load = (async ({ locals, params }) => { + const session = (await locals.auth())!; + if (!verifyCanViewAndEdit(session, parseInt(params.id))) return error(403); + const project = await prisma.projects.findUnique({ + where: { + Id: parseInt(params.id) + }, + select: { + Id: true, + Name: true, + Description: true, + WorkflowProjectUrl: true, + IsPublic: true, + AllowDownloads: true, + DateCreated: true, + DateArchived: true, + Language: true, + ApplicationType: { + select: { + Description: true + } + }, + Organization: { + select: { + Id: true + } + }, + Products: { + select: { + Id: true, + DateUpdated: true, + DatePublished: true, + PublishLink: true, + ProductDefinition: { + select: { + Id: true, + Name: true, + RebuildWorkflowId: true, + RepublishWorkflowId: true, + Workflow: { + select: { + ProductType: true + } + } + } + }, + // Probably don't need to optimize this. Unless it's a really large org, there probably won't be very many of these records for an individual product. In most cases, there will only be zero or one. The only times there will be more is if it's an admin task or an author task. + UserTasks: { + select: { + DateCreated: true, + UserId: true + } + }, + Store: { + select: { + Description: true + } + }, + WorkflowInstance: { + select: { + Id: true, + WorkflowDefinition: { + select: { + Type: true + } + } + } + } + } + }, + Owner: { + select: { + Id: true, + Name: true + } + }, + Group: { + select: { + Id: true, + Name: true + } + }, + Authors: { + select: { + Id: true, + Users: { + select: { + Id: true, + Name: true + } + } + } + }, + Reviewers: { + select: { + Id: true, + Name: true, + Email: true + } + } + } + }); + if (!project) return error(400); + + const organization = await prisma.organizations.findUnique({ + where: { + Id: project.Organization.Id + }, + select: { + OrganizationStores: { + select: { + Store: { + select: { + Id: true, + Name: true, + Description: true, + StoreTypeId: true + } + } + } + } + } + }); + + const transitions = await prisma.productTransitions.findMany({ + where: { + ProductId: { + in: project.Products.map((p) => p.Id) + } + // DateTransition: null + }, + orderBy: [ + { + DateTransition: 'asc' + }, + { + Id: 'asc' + } + ] + }); + const strippedTransitions = project.Products.map((p) => [ + transitions.findLast((tr) => tr.ProductId === p.Id && tr.DateTransition !== null)!, + transitions.find((tr) => tr.ProductId === p.Id && tr.DateTransition === null)! + ]); + // All users who are members of the group and have the author role in the project's organization + // May be a more efficient way to search this, by referencing group memberships instead of users + const authorsToAdd = await prisma.users.findMany({ + where: { + GroupMemberships: { + some: { + GroupId: project?.Group.Id + } + }, + UserRoles: { + some: { + OrganizationId: project?.Organization.Id, + RoleId: RoleId.Author + } + } + } + }); + + const productDefinitions = ( + await prisma.organizationProductDefinitions.findMany({ + where: { + OrganizationId: project.Organization.Id, + ProductDefinition: { + ApplicationTypes: project.ApplicationType + } + }, + select: { + ProductDefinition: { + select: { + Id: true, + Name: true, + Description: true, + Workflow: { + select: { + StoreTypeId: true + } + } + } + } + } + }) + ).map((pd) => pd.ProductDefinition); + + const projectProductDefinitionIds = project.Products.map((p) => p.ProductDefinition.Id); + + const authorForm = await superValidate(valibot(addAuthorSchema)); + const reviewerForm = await superValidate({ language: 'en-us' }, valibot(addReviewerSchema)); + return { + project: { + ...project, + OwnerId: project.Owner.Id, + GroupId: project.Group.Id, + Products: project.Products.map((product) => ({ + ...product, + Transitions: transitions.filter((t) => t.ProductId === product.Id), + PreviousTransition: strippedTransitions.find( + (t) => (t[0] ?? t[1])?.ProductId === product.Id + )?.[0], + ActiveTransition: strippedTransitions.find( + (t) => (t[0] ?? t[1])?.ProductId === product.Id + )?.[1], + actions: getProductActions(product, project.Owner.Id, session.user.userId) + })) + }, + possibleProjectOwners: await prisma.users.findMany({ + where: { + OrganizationMemberships: { + some: { + OrganizationId: project.Organization.Id + } + }, + GroupMemberships: { + some: { + GroupId: project.Group.Id + } + } + } + }), + possibleGroups: await prisma.groups.findMany({ + where: { + OwnerId: project.Organization.Id + } + }), + authorsToAdd, + authorForm, + reviewerForm, + deleteAuthorForm: await superValidate(valibot(deleteAuthorSchema)), + deleteReviewerForm: await superValidate(valibot(deleteReviewerSchema)), + productsToAdd: productDefinitions.filter((pd) => !projectProductDefinitionIds.includes(pd.Id)), + addProductForm: await superValidate(valibot(addProductSchema)), + stores: organization?.OrganizationStores.map((os) => os.Store) ?? [], + actionForm: await superValidate(valibot(projectActionSchema)), + userGroups: (await userGroupsForOrg(session.user.userId, project.Organization.Id)).map( + (g) => g.GroupId + ) + }; +}) satisfies PageServerLoad; + +export const actions = { + async deleteProduct(event) { + if (!verifyCanViewAndEdit((await event.locals.auth())!, parseInt(event.params.id))) + return fail(403); + const form = await superValidate(event.request, valibot(productActionSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + await DatabaseWrites.products.delete(form.data.productId); + }, + async deleteAuthor(event) { + if (!verifyCanViewAndEdit((await event.locals.auth())!, parseInt(event.params.id))) + return fail(403); + const form = await superValidate(event.request, valibot(deleteAuthorSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + const author = await DatabaseWrites.authors.delete({ where: { Id: form.data.id } }); + await Queues.UserTasks.add(`Remove UserTasks for Author #${form.data.id}`, { + type: BullMQ.JobType.UserTasks_Modify, + scope: 'Project', + projectId: parseInt(event.params.id), + operation: { + type: BullMQ.UserTasks.OpType.Delete, + users: [author.UserId], + roles: [RoleId.Author] + } + }); + return { form, ok: true }; + }, + async deleteReviewer(event) { + if (!verifyCanViewAndEdit((await event.locals.auth())!, parseInt(event.params.id))) + return fail(403); + const form = await superValidate(event.request, valibot(deleteReviewerSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + await DatabaseWrites.reviewers.delete({ + where: { + Id: form.data.id + } + }); + return { form, ok: true }; + }, + async addProduct(event) { + if (!verifyCanViewAndEdit((await event.locals.auth())!, parseInt(event.params.id))) + return fail(403); + const form = await superValidate(event.request, valibot(addProductSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + const checkRepository = await prisma.projects.findUnique({ + where: { + Id: parseInt(event.params.id) + }, + select: { + WorkflowProjectUrl: true + } + }); + if (!checkRepository?.WorkflowProjectUrl) { + return error(400, 'Project Repository not Yet Initialized'); + } + const productId = await DatabaseWrites.products.create({ + ProjectId: parseInt(event.params.id), + ProductDefinitionId: form.data.productDefinitionId, + StoreId: form.data.storeId, + WorkflowJobId: 0, + WorkflowBuildId: 0, + WorkflowPublishId: 0 + }); + + return { form, ok: !!productId }; + }, + async productAction(event) { + if (!verifyCanViewAndEdit((await event.locals.auth())!, parseInt(event.params.id))) + return fail(403); + const form = await superValidate(event.request, valibot(productActionSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + const product = await prisma.products.findUnique({ + where: { + Id: form.data.productId + }, + select: { + ProjectId: true + } + }); + if (!product || product.ProjectId !== parseInt(event.params.id)) return fail(404); + await doProductAction(form.data.productId, form.data.productAction); + + return { form, ok: true }; + }, + async addAuthor(event) { + if (!verifyCanViewAndEdit((await event.locals.auth())!, parseInt(event.params.id))) + return fail(403); + const form = await superValidate(event.request, valibot(addAuthorSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + // Appears that CanUpdate is not used TODO + const author = await DatabaseWrites.authors.create({ + data: { + ProjectId: parseInt(event.params.id), + UserId: form.data.author + } + }); + await Queues.UserTasks.add(`Add UserTasks for Author #${author.Id}`, { + type: BullMQ.JobType.UserTasks_Modify, + scope: 'Project', + projectId: parseInt(event.params.id), + operation: { + type: BullMQ.UserTasks.OpType.Create, + users: [form.data.author], + roles: [RoleId.Author] + } + }); + return { form, ok: true }; + }, + async addReviewer(event) { + if (!verifyCanViewAndEdit((await event.locals.auth())!, parseInt(event.params.id))) + return fail(403); + const form = await superValidate(event.request, valibot(addReviewerSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + await DatabaseWrites.reviewers.create({ + data: { + Email: form.data.email, + Name: form.data.name, + Locale: form.data.language, + ProjectId: parseInt(event.params.id) + } + }); + return { form, ok: true }; + }, + async editSettings(event) { + if (!verifyCanViewAndEdit((await event.locals.auth())!, parseInt(event.params.id))) + return fail(403); + const form = await superValidate( + event.request, + valibot( + v.object({ + isPublic: v.boolean(), + allowDownload: v.boolean() + }) + ) + ); + if (!form.valid) return fail(400, { form, ok: false }); + await DatabaseWrites.projects.update(parseInt(event.params.id), { + IsPublic: form.data.isPublic, + AllowDownloads: form.data.allowDownload + }); + return { form, ok: true }; + }, + async editOwnerGroup(event) { + if (!verifyCanViewAndEdit((await event.locals.auth())!, parseInt(event.params.id))) + return fail(403); + const form = await superValidate(event.request, valibot(updateOwnerGroupSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + const success = await DatabaseWrites.projects.update(parseInt(event.params.id), { + GroupId: form.data.group, + OwnerId: form.data.owner + }); + return { form, ok: success }; + }, + async projectAction(event) { + const session = await event.locals.auth(); + if (!session) return fail(403); + const orgId = parseInt(event.params.id!); + if (isNaN(orgId) || !(orgId + '' === event.params.id)) return fail(404); + + const form = await superValidate(event.request, valibot(projectActionSchema)); + if (!form.valid || !form.data.operation || form.data.projectId === null) + return fail(400, { form, ok: false }); + // prefer single project over array + const project = await prisma.projects.findUnique({ + where: { Id: form.data.projectId! }, + select: { + Id: true, + Name: true, + DateArchived: true, + OwnerId: true, + GroupId: true + } + }); + if (!project) return fail(404); + if (!canModifyProject(session, project.OwnerId, orgId)) { + return fail(403); + } + + await doProjectAction( + form.data.operation, + project, + session, + orgId, + form.data.operation === 'claim' + ? (await userGroupsForOrg(session.user.userId, orgId)).map((g) => g.GroupId) + : [] + ); + + return { form, ok: true }; + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/+page.svelte new file mode 100644 index 0000000000..9293e3c265 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/+page.svelte @@ -0,0 +1,747 @@ + + +
+
+
+

+ {data.project?.Name} +

+ + {data.project?.IsPublic ? m.project_public() : m.project_private()} + + - + + {m.project_createdOn()} + + {data.project?.DateCreated ? getRelativeTime(data.project?.DateCreated) : 'null'} + + +
+
+ + + + + +
+
+ +
+
+
+
+

{m.project_details_title()}

+
+ + +
+
+ + + {m.project_details_language()}: + + {data.project?.Language} ({langtagmap.get(data.project.Language ?? '')}) +
+
+ {m.project_details_type()}: + {data.project?.ApplicationType.Description} +
+
+
+ {m.project_projectDescription()}: +
+

{data.project?.Description}

+
+ {#if data.project?.WorkflowProjectUrl} +
+ {m.project_side_repositoryLocation()}: +
+

+ {data.project?.WorkflowProjectUrl} +

+
+ {/if} +
+
+
+

{m.project_products_title()}

+
+ {m.products_definition()} +
+
+ + + + + +
+
+ {#if !data.project?.Products.length} + {m.projectTable_noProducts()} + {:else} + {@const langTag = languageTag()} + {#each data.project.Products.sort( (a, b) => sortByName(a.ProductDefinition, b.ProductDefinition, langTag) ) as product} +
+
+ + + {product.ProductDefinition.Name} + + {#if product.PublishLink} + {@const pType = product.ProductDefinition.Workflow.ProductType} + + + + + {#if pType !== ProductType.Web} + + + + {/if} + + {/if} + + {m.project_products_updated()} +
+ + {getRelativeTime(product.DateUpdated)} + +
+ + {m.project_products_published()} +
+ + {getRelativeTime(product.DatePublished)} + +
+ + + +
+ {#if product.WorkflowInstance} +
+ + {m.tasks_waiting({ + // waiting since EITHER (the last task exists) -> that task's creation time + // OR (there are no tasks for this product) -> the last completed transition's completion time + waitTime: getRelativeTime( + product.UserTasks.slice(-1)[0]?.DateCreated ?? + product.PreviousTransition?.DateTransition ?? + null + ) + })} + + {m.tasks_forNames({ + allowedNames: product.ActiveTransition?.AllowedUserNames || m.appName(), + activityName: product.ActiveTransition?.InitialState ?? '' + // activityName appears to show up blank primarily at the very startup of a new product? + })} + {#if product.UserTasks.slice(-1)[0]?.UserId === page.data.session?.user.userId} + + {m.common_continue()} + + {/if} +
+ {/if} + +
+ {/each} + {/if} +
+
+
+
+

{m.project_settings_title()}

+
+ ({ update }) => + update({ reset: false })} + > +
+ + +
+
+
+
+
+
+ ({ update }) => + update({ reset: false })} + > +
+
+ + + {m.project_side_organization()} + + + {data.organizations.find((o) => data.project?.Organization.Id === o.Id)?.Name} + +
+
+
+ + + {m.project_side_projectOwner()} + + + + +
+
+
+ + + {m.project_side_projectGroup()} + + + + +
+
+
+
+ +
+
+

{m.project_side_authors_title()}

+
+
+ {#if data.project?.Authors.length ?? 0 > 0} + {#each data.project?.Authors ?? [] as author} +
+ {author.Users.Name} +
+ + +
+
+ {/each} + {:else} +

{m.project_side_authors_empty()}

+ {/if} +
+
+
+
+ + +
+
+
+
+
+
+

{m.project_side_reviewers_title()}

+
+
+ {#if data.project?.Reviewers.length ?? 0 > 0} + {#each data.project?.Reviewers ?? [] as reviewer} +
+ {reviewer.Name} ({reviewer.Email}) +
+ + +
+
+ {/each} + {:else} +

{m.project_side_reviewers_empty()}

+ {/if} +
+
+
+
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/edit/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/edit/+page.server.ts new file mode 100644 index 0000000000..584ef365c4 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/edit/+page.server.ts @@ -0,0 +1,68 @@ +import { idSchema } from '$lib/valibot'; +import { error } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import { verifyCanViewAndEdit } from '$lib/projects/common.server'; +import type { Actions, PageServerLoad } from './$types'; + +const projectPropertyEditSchema = v.object({ + name: v.pipe(v.string(), v.minLength(1)), + group: idSchema, + owner: idSchema, + language: v.string(), + description: v.nullable(v.string()) +}); + +export const load = (async ({ locals, params }) => { + if (!verifyCanViewAndEdit((await locals.auth())!, parseInt(params.id))) return error(403); + const project = await prisma.projects.findUnique({ + where: { + Id: parseInt(params.id) + } + }); + if (!project) return error(400); + const owners = await prisma.users.findMany({ + where: { + OrganizationMemberships: { + some: { + OrganizationId: project.OrganizationId + } + } + } + }); + const groups = await prisma.groups.findMany({ + where: { + OwnerId: project.OrganizationId + } + }); + const form = await superValidate( + { + name: project.Name!, + group: project.GroupId, + owner: project.OwnerId, + language: project.Language!, + description: project.Description + }, + valibot(projectPropertyEditSchema) + ); + return { project, form, owners, groups }; +}) satisfies PageServerLoad; + +export const actions: Actions = { + default: async function (event) { + if (!verifyCanViewAndEdit((await event.locals.auth())!, parseInt(event.params.id))) return fail(403); + const form = await superValidate(event.request, valibot(projectPropertyEditSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + if (isNaN(parseInt(event.params.id))) return fail(400, { form, ok: false }); + const success = await DatabaseWrites.projects.update(parseInt(event.params.id), { + Name: form.data.name, + GroupId: form.data.group, + OwnerId: form.data.owner, + Language: form.data.language, + Description: form.data.description ?? '' + }); + return { form, ok: success }; + } +}; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/edit/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/edit/+page.svelte new file mode 100644 index 0000000000..eff1af6154 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/[id=idNumber]/edit/+page.svelte @@ -0,0 +1,79 @@ + + +
+
+

{m.project_settings_title()}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+ +
+
+ {m.common_cancel()} + +
+
+
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/import/[id=idNumber]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/import/[id=idNumber]/+page.server.ts new file mode 100644 index 0000000000..50282474a7 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/import/[id=idNumber]/+page.server.ts @@ -0,0 +1,214 @@ +import { importJSONSchema } from '$lib/projects/common'; +import { verifyCanCreateProject } from '$lib/projects/common.server'; +import { idSchema } from '$lib/valibot'; +import { error, redirect } from '@sveltejs/kit'; +import { BullMQ, DatabaseWrites, prisma, Queues } from 'sil.appbuilder.portal.common'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const projectsImportSchema = v.object({ + group: idSchema, + type: idSchema, + // I would use v.file, but that is not supported until after Node 18 + json: importJSONSchema +}); + +export const load = (async ({ locals, params }) => { + if (!verifyCanCreateProject((await locals.auth())!, parseInt(params.id))) return error(403); + + const organization = await prisma.organizations.findUnique({ + where: { + Id: parseInt(params.id) + }, + select: { + Id: true, + Groups: { + select: { + Id: true, + Name: true + } + }, + OrganizationProductDefinitions: { + select: { + ProductDefinition: { + select: { + ApplicationTypes: { + select: { + Id: true, + Description: true + } + } + } + } + } + } + } + }); + + if (!organization) return error(404); + + const types = await prisma.applicationTypes.findMany({ + select: { + Id: true, + Description: true + } + }); + + const form = await superValidate( + { + group: organization?.Groups[0]?.Id ?? undefined, + type: types?.[0].Id ?? undefined + }, + valibot(projectsImportSchema), + { errors: false } + ); + return { form, organization, types }; +}) satisfies PageServerLoad; + +export const actions: Actions = { + default: async function (event) { + const session = (await event.locals.auth())!; + + const organizationId = parseInt(event.params.id); + + if (isNaN(organizationId)) return error(404); + + if (!verifyCanCreateProject(session, organizationId)) return error(403); + + const form = await superValidate(event.request, valibot(projectsImportSchema)); + if (!form.valid) { + return fail(400, { form, ok: false, errors: form.errors }); + } + + const organization = await prisma.organizations.findUnique({ + where: { + Id: organizationId + }, + select: { + Name: true + } + }); + + if (!organization) return error(404); + try { + const errors: { + path: string; + messages: string[]; + }[] = []; + + await Promise.all( + form.data.json.Products.map(async (p, i) => { + const pdi = ( + await prisma.productDefinitions.findFirst({ + where: { + Name: p.Name, + OrganizationProductDefinitions: { + some: { + OrganizationId: organizationId + } + } + }, + select: { + Id: true + } + }) + )?.Id; + + if (pdi === undefined) { + // TODO: better errors + errors.push({ + path: `json.Products[${i}].Name`, + messages: [ + `Could not find ProductDefinition: "${p.Name}" for Organization: ${organization.Name}` + ] + }); + } + + const si = ( + await prisma.stores.findFirst({ + where: { + Name: p.Store, + OrganizationStores: { + some: { + OrganizationId: organizationId + } + } + }, + select: { + Id: true + } + }) + )?.Id; + + if (si === undefined) { + // TODO: better errors + errors.push({ + path: `json.Products[${i}].Store`, + messages: [ + `Could not find Store: "${p.Store}" for Organization: ${organization.Name}` + ] + }); + } + return { + ProductDefinitionId: pdi, + StoreId: si + }; + }) + ); + + if (!errors.length) { + const imp = await DatabaseWrites.projectImports.create({ + data: { + ImportData: JSON.stringify(form.data.json), + TypeId: form.data.type, + OwnerId: session.user.userId, + GroupId: form.data.group, + OrganizationId: organizationId + }, + select: { + Id: true + } + }); + const projects = await DatabaseWrites.projects.createMany( + form.data.json.Projects.map((pj) => { + return { + OrganizationId: organizationId, + Name: pj.Name, + GroupId: form.data.group, + OwnerId: session.user.userId, + Language: pj.Language, + TypeId: form.data.type, + Description: pj.Description ?? '', + IsPublic: pj.IsPublic, + ImportId: imp.Id + }; + }) + ); + + if (projects) { + // Create products + await Queues.Miscellaneous.addBulk( + projects.map((p) => ({ + name: `Create Project #${p}`, + data: { + type: BullMQ.JobType.Project_Create, + projectId: p + } + })) + ); + } + } + + if (errors.length) { + return fail(400, { form, ok: false, errors }); + } + + return redirect(302, `/projects/own/${organizationId}`); + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +}; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/import/[id=idNumber]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/import/[id=idNumber]/+page.svelte new file mode 100644 index 0000000000..cbcc10e87e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/import/[id=idNumber]/+page.svelte @@ -0,0 +1,164 @@ + + +
+
+

{m.project_importProjects()}

+ + {m.project_importProjectsHelp()} + +
+ + + +
+ {#if $allErrors.length} +
    + {#each $allErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} + {#if returnedErrors.length} +
    + {#each returnedErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} + {#if parseErrors} +
    + {#each parseErrors.root ?? [] as error} +
  • + {error} +
  • + {/each} + {#if parseErrors.nested} + {#each Object.entries(parseErrors.nested) as error} +
  • + {error[0]}: + {error[1]?.join('. ')} +
  • + {/each} + {/if} + {#each parseErrors.other ?? [] as error} +
  • + {error} +
  • + {/each} +
+ {/if} +
+ {m.common_cancel()} + +
+
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/new/[id=idNumber]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/new/[id=idNumber]/+page.server.ts new file mode 100644 index 0000000000..4fdb13c179 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/new/[id=idNumber]/+page.server.ts @@ -0,0 +1,85 @@ +import { projectCreateSchema } from '$lib/projects/common'; +import { verifyCanCreateProject } from '$lib/projects/common.server'; +import { error, redirect } from '@sveltejs/kit'; +import { BullMQ, DatabaseWrites, prisma, Queues } from 'sil.appbuilder.portal.common'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import type { Actions, PageServerLoad } from './$types'; + +export const load = (async ({ locals, params }) => { + if (!verifyCanCreateProject((await locals.auth())!, parseInt(params.id))) return error(403); + + const organization = await prisma.organizations.findUnique({ + where: { + Id: parseInt(params.id) + }, + select: { + Groups: { + select: { + Id: true, + Name: true + } + }, + PublicByDefault: true + } + }); + + // There shouldn't actually be any restriction on this + const types = await prisma.applicationTypes.findMany({ + select: { + Id: true, + Description: true + } + }); + + const form = await superValidate( + { + group: organization?.Groups[0]?.Id ?? undefined, + type: types?.[0].Id ?? undefined, + IsPublic: organization?.PublicByDefault ?? undefined + }, + valibot(projectCreateSchema), + { errors: false } + ); + return { form, organization, types }; +}) satisfies PageServerLoad; + +export const actions: Actions = { + default: async function (event) { + const session = (await event.locals.auth())!; + const organizationId = parseInt(event.params.id); + const form = await superValidate(event.request, valibot(projectCreateSchema)); + if (isNaN(organizationId)) return error(404); + if (!verifyCanCreateProject(session, organizationId)) return error(403); + if (!form.valid) { + return fail(400, { form, ok: false, errors: form.errors }); + } + const project = await DatabaseWrites.projects.create({ + OrganizationId: organizationId, + Name: form.data.Name, + GroupId: form.data.group, + OwnerId: session.user.userId, + Language: form.data.Language, + TypeId: form.data.type, + Description: form.data.Description ?? '', + IsPublic: form.data.IsPublic + }); + + if (project !== false) { + await Queues.Miscellaneous.add( + `Create Project #${project}`, + { + type: BullMQ.JobType.Project_Create, + projectId: project as number + }, + BullMQ.Retry5e5 + ); + return redirect(302, `/projects/${project}`); + } + return { + form, + ok: false, + errors: [{ path: 'root', messages: ['Project could not be created.'] }] + }; + } +}; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/new/[id=idNumber]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/new/[id=idNumber]/+page.svelte new file mode 100644 index 0000000000..e9ce8890c4 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/projects/new/[id=idNumber]/+page.svelte @@ -0,0 +1,124 @@ + + +
+
+

{m.project_newProject()}

+
+
+ + +
+
+ + + +
+
+ +
+ +

{m.project_visibilityDescription()}

+
+
+
+ {#if $allErrors.length} +
    + {#each $allErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} +
+ {m.common_cancel()} + +
+
+
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/+page.server.ts new file mode 100644 index 0000000000..3724ebbb9f --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/+page.server.ts @@ -0,0 +1,25 @@ +import { prisma } from 'sil.appbuilder.portal.common'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async (event) => { + const session = await event.locals.auth(); + const tasks = await prisma.userTasks.findMany({ + where: { + UserId: session?.user.userId + }, + include: { + Product: { + include: { + Project: true, + ProductDefinition: { + include: { + Workflow: true + } + } + } + } + }, + distinct: 'ProductId' + }); + return { tasks }; +}; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/+page.svelte new file mode 100644 index 0000000000..583869cb7b --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/+page.svelte @@ -0,0 +1,110 @@ + + +
+

{m.tasks_title()}

+
+ {#if data.tasks.length > 0} + {@const langTag = languageTag()} + + + + + + + + + + {#each data.tasks as task} + goto(`/tasks/${task.ProductId}`)}> + + + + + {#if task.Comment} + + + + {/if} + {/each} + +
{m.tasks_product()}{m.tasks_project()}{m.tasks_waitTime()}
+ + + + {task.Product.ProductDefinition.Name} + + + + {task.Status} + + + + {task.Product.Project.Name} + + + + {task.DateUpdated ? getRelativeTime(task.DateUpdated) : 'null'} + +
+ {#if task.Comment.startsWith('system.')} + {#if task.Comment.startsWith('system.build-failed')} + + {m.system_buildFailed()} + + {:else if task.Comment.startsWith('system.publish-failed')} + + {m.system_publishFailed()} + + {/if} +
+ + {m.project_products_publications_console()} + + {:else} + {task.Comment} + {/if} +
+ {:else} +
+

{m.tasks_noTasksTitle()}

+ {m.tasks_noTasksDescription()} +
+ {/if} +
+
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts new file mode 100644 index 0000000000..c5c972c5a4 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts @@ -0,0 +1,222 @@ +import { isSuperAdmin } from '$lib/utils'; +import type { Session } from '@auth/sveltekit'; +import { error, redirect } from '@sveltejs/kit'; +import { prisma, Workflow } from 'sil.appbuilder.portal.common'; +import { RoleId } from 'sil.appbuilder.portal.common/prisma'; +import { WorkflowAction, WorkflowState } from 'sil.appbuilder.portal.common/workflow'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const sendActionSchema = v.object({ + state: v.enum(WorkflowState), + flowAction: v.enum(WorkflowAction), + comment: v.string() +}); + +type Fields = { + ownerName?: string; //Product.Project.Owner.Name + ownerEmail?: string; //Product.Project.Owner.Email + projectName: string; //Product.Project.Name + projectDescription: string; //Product.Project.Description + storeDescription?: string; //Product.Store.Description + listingLanguageCode?: string; //Product.StoreLanguage.Name + projectURL?: string; //Product.Project.WorkflowAppProjectURL + displayProductDescription: boolean; //Product.ProductDefinition.Description + appType?: string; //Product.ProductDefinition.ApplicationTypes.Description + projectLanguageCode?: string; //Product.Project.Language +}; + +export const load = (async ({ params, url, locals }) => { + const session = await locals.auth(); + if (!(await verifyCanViewTask(session!, params.product_id))) return error(403); + const snap = await Workflow.getSnapshot(params.product_id); + if (!snap) return error(404); + + const product = await prisma.products.findUnique({ + where: { + Id: params.product_id + }, + select: { + WorkflowBuildId: true, + Project: { + select: { + Id: true, + Name: true, + Description: true, + WorkflowAppProjectUrl: snap?.context.includeFields.includes('projectURL'), + Language: snap?.context.includeFields.includes('projectLanguageCode'), + Owner: { + select: { + Id: true, + Name: snap?.context.includeFields.includes('ownerName'), + Email: snap?.context.includeFields.includes('ownerEmail') + } + }, + //conditionally include reviewers + Reviewers: snap?.context.includeReviewers + ? { + select: { + Name: true, + Email: true + } + } + : undefined, + Authors: { + select: { + UserId: true + } + }, + Organization: { + select: { + UserRoles: { + where: { + RoleId: RoleId.OrgAdmin + } + } + } + } + } + }, + Store: snap?.context.includeFields.includes('storeDescription') + ? { + select: { + Description: true + } + } + : undefined, + StoreLanguage: snap?.context.includeFields.includes('listingLanguageCode') + ? { + select: { + Name: true + } + } + : undefined, + ProductDefinition: { + select: { + Name: true, + ApplicationTypes: snap?.context.includeFields.includes('appType') + ? { + select: { + Description: true + } + } + : undefined + } + } + } + }); + + const artifacts = snap?.context.includeArtifacts + ? await prisma.productArtifacts.findMany({ + where: { + ProductId: params.product_id, + ProductBuild: { + BuildId: product?.WorkflowBuildId + }, + //filter by artifact type + ArtifactType: + typeof snap.context.includeArtifacts === 'string' + ? snap.context.includeArtifacts + : undefined //include all + }, + select: { + ArtifactType: true, + FileSize: true, + Url: true + } + }) + : []; + + return { + actions: Workflow.availableTransitionsFromName(snap.state, snap.config) + .filter((a) => { + if (session?.user.userId === undefined) return false; + switch (a[0].meta?.user) { + case RoleId.AppBuilder: + return session.user.userId === product?.Project.Owner.Id; + case RoleId.Author: + return product?.Project.Authors.map((a) => a.UserId).includes(session.user.userId); + case RoleId.OrgAdmin: + return product?.Project.Organization.UserRoles.map((u) => u.UserId).includes( + session.user.userId + ); + default: + return false; + } + }) + .map((a) => a[0].eventType as WorkflowAction), + taskTitle: snap?.state, + instructions: snap?.context.instructions, + projectId: product?.Project.Id, + productDescription: product?.ProductDefinition.Name, + fields: { + projectName: product?.Project.Name, + projectDescription: product?.Project?.Description, + ownerName: product?.Project.Owner.Name, + ownerEmail: product?.Project.Owner.Email, + storeDescription: product?.Store?.Description, + listingLanguageCode: product?.StoreLanguage?.Name, + projectURL: product?.Project.WorkflowAppProjectUrl, + displayProductDescription: snap?.context.includeFields.includes('productDescription'), + appType: product?.ProductDefinition.ApplicationTypes?.Description, + projectLanguageCode: product?.Project.Language + } as Fields, + files: artifacts, + reviewers: product?.Project.Reviewers, + taskForm: await superValidate( + { + state: snap?.state as WorkflowState + }, + valibot(sendActionSchema) + ) + }; +}) satisfies PageServerLoad; + +export const actions = { + default: async ({ request, params, locals }) => { + const session = await locals.auth(); + if (!(await verifyCanViewTask(session, params.product_id))) return error(403); + const form = await superValidate(request, valibot(sendActionSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + + const flow = await Workflow.restore(params.product_id); + + if (!flow) return fail(404, { form, ok: false }); + + //double check that state matches current snapshot + if (form.data.state === flow.state()) { + flow.send({ + type: form.data.flowAction, + comment: form.data.comment, + userId: session?.user.userId ?? null + }); + } + + const product = await prisma.products.findUnique({ + where: { Id: params.product_id }, + select: { ProjectId: true } + }); + + redirect(302, `/projects/${product?.ProjectId}`); + } +} satisfies Actions; + +// allowed if SuperAdmin, or the user has a UserTask for the Product +async function verifyCanViewTask(session: Session | null, productId: string): Promise { + if (!session) return false; + + return ( + isSuperAdmin(session.user.roles) || + !!(await prisma.userTasks.findFirst({ + where: { + ProductId: productId, + UserId: session.user.userId + }, + select: { + Id: true + } + })) + ); +} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.svelte new file mode 100644 index 0000000000..8fd98ea83a --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.svelte @@ -0,0 +1,294 @@ + + +
+
+ +
+
+ {#if data.actions?.length} +
+ {#each data.actions as action} + + {/each} +
+ {/if} + + +
+
+

+ {data.taskTitle} +

+
+ {#if data.fields.ownerName && data.fields.ownerEmail} +
+ + +
+ {/if} +
+ + +
+ {#if data.fields.storeDescription} +
+ + {#if data.fields.listingLanguageCode} + + {/if} +
+ {/if} + {#if data.fields.projectURL} + + {/if} + {#if data.fields.displayProductDescription && data.fields.appType && data.fields.projectLanguageCode} + + + + {/if} +
+ {#if data.instructions} + + {@const SvelteComponent = instructions[data.instructions]} +
+ +
+ {/if} + {#if data?.files?.length} +

{m.products_files_title()}

+
+ f.ArtifactType, + sortable: true + }, + { + id: 'fileSize', + header: m.project_products_size(), + data: (f) => f.FileSize, + render: (s) => bytesToHumanSize(s), + sortable: true + }, + { + id: 'url', + header: m.tasks_files_link(), + data: (f) => f.Url, + render: (u) => `${u}` + } + ]} + onRowClick={(data) => { + window.open(data.Url, '_blank')?.focus(); + }} + /> +
+ {/if} + {#if data?.reviewers?.length} +

{m.project_side_reviewers_title()}

+
+ r.Name, + sortable: true + }, + { + id: 'email', + header: m.profile_email(), + data: (r) => r.Email, + sortable: true + } + ]} + /> +
+ {/if} +
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/App_Configuration.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/App_Configuration.svelte new file mode 100644 index 0000000000..e0a3bf480c --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/App_Configuration.svelte @@ -0,0 +1,25 @@ +

Instructions

+
    +
  • + In the Publishing > App Publishing configuration page: +
      +
    • + Enable Scriptoria by selecting the We would like to use Scriptoria option. +
    • +
    +
  • +
  • + In the Publishing > Scriptoria configuration page: +
      +
    • + Copy and Paste the above App Project URL into the Enter App Project URL field. +
    • +
    • + Click Login... to connect to Scriptoria. +
    • +
    • + Click Upload and successfully upload the data to the app publishing repository. +
    • +
    +
  • +
\ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Approval_Pending.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Approval_Pending.svelte new file mode 100644 index 0000000000..cd7d102cc0 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Approval_Pending.svelte @@ -0,0 +1,13 @@ +

Instructions

+

Communicate with the end user if you have questions about whether this project should be published.

+
    +
  • + Select Reject to terminate this product request. +
  • +
  • + Select Hold to update the comment on the task. +
  • +
  • + Select Approve to continue with the product. +
  • +
\ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Asset_Package_Verify_And_Publish.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Asset_Package_Verify_And_Publish.svelte new file mode 100644 index 0000000000..02e73037ec --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Asset_Package_Verify_And_Publish.svelte @@ -0,0 +1,12 @@ +

Instructions

+
    +
  • + Get preview link by clicking on the entry with type asset-preview in the Product Files grid view. +
  • +
  • + Test the asset packages by setting the Container App URL in a Container APP project to the preview link. Then Run the iOS Container App in an iOS Simulator. +
  • +
  • + Click Approve when you are satisfied that everything is ready to go and the app will be published. +
  • +
\ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Authors_Download.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Authors_Download.svelte new file mode 100644 index 0000000000..ea14473075 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Authors_Download.svelte @@ -0,0 +1,51 @@ +

Instructions

+

To be sure you are updating the latest version of this published project, please download the project before making changes. This can be done in one of two different ways:

+
    +
  1. + Download a new copy +
      +
    • + From the File menu, select Open App From Scriptoria... +
    • +
    • + Copy and Paste the above App Project URL into the Enter the project URL field. +
    • +
    • + Click Login to connect to Scriptoria. +
    • +
    • + Click Download to download a new copy of the project from the app publishing repository. +
    • +
    • + Click OK to add the project to you list of projects. +
    • +
    +
  2. +
  3. + Update current project +
      +
    • + In the Publishing > App Publishing configuration page: +
        +
      • + Enable Scriptoria by selecting the We would like to use Scriptoria option (if not already selected). +
      • +
      +
    • +
    • + In the Publishing > Scriptoria configuration page: +
        +
      • + Copy and Paste the above App Project URL into the Enter App Project URL field (if not already set). +
      • +
      • + Click Login to connect to Scriptoria. +
      • +
      • + Click Download and successfully update the project from the app publishing repository. +
      • +
      +
    • +
    +
  4. +
\ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Authors_Upload.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Authors_Upload.svelte new file mode 100644 index 0000000000..03131323e5 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Authors_Upload.svelte @@ -0,0 +1,9 @@ +

Instructions

+
    +
  • + Make any changes needed in the app. +
  • +
  • + Click Upload in the Publishing > Scriptoria app configuration page and successfully upload the data to the app publishing repository. +
  • +
\ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Create_App_Entry.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Create_App_Entry.svelte new file mode 100644 index 0000000000..05a4b6444d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Create_App_Entry.svelte @@ -0,0 +1,139 @@ +

Instructions

+
    +
  • Download the AAB file by clicking on the entry with type aab in the Product Files grid view.
  • +
  • Go to Google Play Developer Console and sign in.
  • +
  • + Click Create app button and complete the following items: +
      +
    • Set App name to Project Name shown above.
    • +
    • Set Default language to Store Listing Language shown above.
    • +
    • Set App or game to App.
    • +
    • Set Free or paid to Free.
    • +
    • + Check Confirm app meets the Developer Program Policies checkbox. + +
    • +
    • Check Accept US export laws checkbox.
    • +
    • Click the Create app button.
    • +
    +
  • +
  • + Select App content under the Policy section in the menu on the left and do the following: +
      +
    • + Under the Privacy policy section, click the Start link. +
        +
      • + Fill in the URL for your app's privacy policy. +
      • +
      • For an example, see the Wycliffe Scripture App Privacy Policy
      • +
      • Click the Save button at the bottom of the page.
      • +
      • Click the App content link at the top of the page to go back.
      • +
      +
    • +
    • + Under the Ads section, click the Start link. +
        +
      • Click on No, my app does not contain ads radio button.
      • +
      • Click the Save button at the bottom of the page.
      • +
      • Click the App content link at the top of the page to go back.
      • +
      +
    • +
    • + Under the App access section, click the Start link. +
        +
      • Click the appropriate1 radio button based on features used in the app.
      • +
      • Click the Save button at the bottom of the page.
      • +
      • Click the App content link at the top of the page to go back.
      • +
      +
    • +
    • + Under the Content ratings section, click the Start link. +
        +
      • Click the Start questionnaire button and complete the questionnaire.
      • +
      • Click the Save button at the bottom of the page.
      • +
      • Click the Next button at the bottom of the page.
      • +
      • Review the ratings applied and then click the Submit button at the bottom of the page.
      • +
      • Click the App content link at the top of the page to go back.
      • +
      +
    • +
    • + Under the Target audience and content section, click the Start link. +
        +
      • Check the appropriate2 Target age groups for the app (we suggest 13-15, 16-17, and 18 and over).
      • +
      • Click the Next button at the bottom of the page.
      • +
      • For the Appeals to children option, click the No radio button.
      • +
      • Click the Next button at the bottom of the page.
      • +
      • Click the Save button at the bottom of the page.
      • +
      • Click the App content link at the top of the page to go back.
      • +
      +
    • +
    • + Under the News apps section, click the Start link. +
        +
      • Click on No, my app does not contain ads radio button.
      • +
      • Click the Save button at the bottom of the page.
      • +
      • Click the App content link at the top of the page to go back.
      • +
      +
    • +
    • + Under the COVID-19 contract tracing and status apps section, click the Start link. +
        +
      • Click on the My app is not a publicly available COVID-19 contact tracing or status app checkbox.
      • +
      • Click the Save button at the bottom of the page.
      • +
      • Click the App content link at the top of the page to go back.
      • +
      +
    • +
    • + Under the Data safety section, click the Start link. +
        +
      • Download the Data Safety CSV by clicking on the entry with type data-safety-csv in the Product Files grid view.
      • +
      • Click the Import from CSV button at the top of the page.
      • +
      • In the Import from CSV dialog, click on the Upload link and select the downloaded Data Safety CSV (or drag and drop the file into the file upload area).
      • +
      • Click the Submit button at the bottom of the page.
      • +
      • Click the App content link at the top of the page to go back.
      • +
      +
    • +
    • + Under the Advertising ID section, click the Start link. +
        +
      • For the Does your app use advertising ID? question, click the No radio button.
      • +
      • Click the Save button at the bottom of the page.
      • +
      • Click the App content link at the top of the page to go back.
      • +
      +
    • +
    +
  • +
  • Select Production under the Release section in the menu on the left.
  • +
  • + On the right, click the Countries / regions link and do the following: +
      +
    • Click the Add countries / regions link.
    • +
    • Select the countries where the app will be available. You can click the checkbox next to Country / region to select all countries.
    • +
    • Click the Add countries / regions button at the bottom of the page.
    • +
    • Click the Add button in the Add countries / regions? dialog.
    • +
    +
  • +
  • + On the right, click Create new release button at the top of the page and complete the following items: +
      +
    • In the App bundles section, click the Choose signing key link.
    • +
    • In the Choose signing key dialog, click the Use Google-generate key button.
    • +
    • Wait for the App signing key generated message at the bottom of the window.
    • +
    • Click the Upload link and select the downloaded AAB (or drag and drop the file into the file upload area).
    • +
    +
  • +
  • Click the Save link at the bottom of the page.
  • +
  • Click the Review release button and ignore the Errors1 and Warnings2 for now.
  • +
+

Notes

+
    +
  1. + The errors are due to missing store listing information which will be uploaded later by Scriptoria. +
  2. +
  3. + The warning is related to the upload containing native code and not uploading debug symbols. +
  4. +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/GooglePlay_Configuration.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/GooglePlay_Configuration.svelte new file mode 100644 index 0000000000..c92477e943 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/GooglePlay_Configuration.svelte @@ -0,0 +1,39 @@ +

Instructions

+
    +
  • + In the Publishing > App Publishing configuration page: +
      +
    • Enable Scriptoria by selecting the We would like to use Scriptoria option.
    • +
    +
  • +
  • + In the Publishing > Google Play Store Listing configuration page: +
      +
    • Fill out the Play Listing information.
    • +
    +
  • +
  • + In the Publishing > Publishing Properties configuration page: +
      +
    • Fill out any needed publishing properties.
    • +
    • For new apps, set BUILD_ANDROID_AAB to the value 1.
    • +
    • For new apps, set BUILD_KEYSTORE to the name of a keystore not used to publish APKs.
    • +
    • See the Publishing Properties Documentation for more details.
    • +
    +
  • +
  • + In the Publishing > Scriptoria configuration page: +
      +
    • Copy and Paste the above App Project URL into the Enter App Project URL field.
    • +
    • Click Login... to connect to Scriptoria.
    • +
    • Click Upload and successfully upload the data to the app publishing repository.
    • +
    +
  • +
  • + To continue: +
      +
    • If this is a new app that has not been uploaded to Google Play yet, click New App.
    • +
    • If this is an app that already exists in Google Play, click Existing App.
    • +
    +
  • +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/GooglePlay_Verify_And_Publish.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/GooglePlay_Verify_And_Publish.svelte new file mode 100644 index 0000000000..568f846fbc --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/GooglePlay_Verify_And_Publish.svelte @@ -0,0 +1,12 @@ +

Instructions

+
    +
  • Download the APK or AAB file by clicking on the entry with type apk or aab in the Product Files grid view.
  • +
  • Test the app1. See Installing Android Apps For Testing for help.
  • +
  • Open the preview of the store listing by clicking on the entry with type play-listing in the Product Files grid view.
  • +
  • Click Approve when you are satisfied that everything is ready to go and the app will be published.2
  • +
+

Notes

+
    +
  1. If the "Share app Installer file" feature is enabled, it will be present in the APK and not present in the AAB.
  2. +
  3. The first time the app is published, the Organization Admin will need to do extra work to make it live. After the first time, it will be available in a shorter amount of time.
  4. +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Make_It_Live.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Make_It_Live.svelte new file mode 100644 index 0000000000..8d29463e8e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Make_It_Live.svelte @@ -0,0 +1,39 @@ +

Instructions

+

+ For apps being added to Scriptoria that are already published to Google Play, most of these steps have already been completed. You will still need to perform the last step to complete the rollout of the draft publish of the new version of + the app. +

+
    +
  • Go to Google Play Developer Console and sign in.
  • +
  • Select Store presence under the Grow section in the menu on the left. This will expand the items below it.
  • +
  • Select Store settings in the menu on the left and do the following:
  • +
  • +
      +
    • + Set Category to Books & Reference (or appropriate category) +
        +
      • Click on Manage tags link and set appropriate tags (e.g. Religious text) and click the Apply button at the bottom of the page.
      • +
      • Set Email address
      • +
      • Set Website (optional)
      • +
      • Click the Save button at the bottom of the page.
      • +
      +
    • +
    +
  • +
  • +

    To complete the rollout, Select Production in the menu on the left and do the following:

    +
  • +
  • +

    Click the Edit release button at the top of the page.

    +
  • +
  • Click the Review release button at the bottom of the page.
  • +
  • Click the Start rollout to Production button at the bottom of the page.
  • +
+

Notes

+
    +
  1. If the app uses one of the Restricted Users setting in Security, then you will need to provide instructions on how a reviewer can gain access.
  2. +
  3. + If your app isn't specifically targeted to appeal to children (like a Children's Bible or Bible stories picture book), then you should specify that it is intended for 13+ otherwise there are additional regulations that have to + be met. +
  4. +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Readiness_Check.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Readiness_Check.svelte new file mode 100644 index 0000000000..c156ba0117 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Readiness_Check.svelte @@ -0,0 +1,8 @@ +

Instructions

+

Press Continue when all the following are true.

+
    +
  • I have reviewed the Intellectual Property (IP) Attributions Guidelines.
  • +
  • I have used the appropriate App Builder on my computer to define and compile my app. I have tested it and believe it is ready to publish widely.
  • +
  • I have been given permission by my organization to publish the contents of the app.
  • +
  • I know who should be listed as copyright owners for the text and audio.
  • +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Synchronize_Data.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Synchronize_Data.svelte new file mode 100644 index 0000000000..84ea1637fd --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Synchronize_Data.svelte @@ -0,0 +1,11 @@ +

Instructions

+
    +
  • Make any changes needed in the app.
  • +
  • + In the Publishing > Scriptoria configuration page: +
      +
    • Click Login... to connect to Scriptoria.
    • +
    • Click Upload and successfully upload the data to the app publishing repository.
    • +
    +
  • +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Verify_And_Publish.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Verify_And_Publish.svelte new file mode 100644 index 0000000000..228506f837 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Verify_And_Publish.svelte @@ -0,0 +1,6 @@ +

Instructions

+
    +
  • Download the APK file by clicking on the entry with type apk in the Product Files grid view.
  • +
  • Test the app. See Installing Android Apps For Testing for help.
  • +
  • Click Approve when you are satisfied that everything is ready to go and the app will be published.
  • +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Waiting.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Waiting.svelte new file mode 100644 index 0000000000..bb2766417d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Waiting.svelte @@ -0,0 +1,2 @@ +

Scriptoria is Busy

+

Scriptoria is busy working on your product. You will receive a notification and an entry will be in your My Tasks when you have something to do.

\ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Web_Verify.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Web_Verify.svelte new file mode 100644 index 0000000000..acbc174e60 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/Web_Verify.svelte @@ -0,0 +1,6 @@ +

Instructions

+
    +
  • Download a zip file of the web site by clicking on the entry with type html or pwa in the Product Files grid view.
  • +
  • Extract the zip file and test the web site.
  • +
  • Click Approve when you are satisfied that everything is ready to go and the app will be published.
  • +
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/index.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/index.ts new file mode 100644 index 0000000000..e2ced84ace --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/instructions/index.ts @@ -0,0 +1,33 @@ +import Asset_Package_Verify_And_Publish from "./Asset_Package_Verify_And_Publish.svelte"; +import App_Configuration from "./App_Configuration.svelte"; +import Create_App_Entry from "./Create_App_Entry.svelte"; +import Authors_Download from "./Authors_Download.svelte"; +import GooglePlay_Configuration from "./GooglePlay_Configuration.svelte"; +import GooglePlay_Verify_And_Publish from "./GooglePlay_Verify_And_Publish.svelte"; +import Make_It_Live from "./Make_It_Live.svelte"; +import Approval_Pending from "./Approval_Pending.svelte"; +import Readiness_Check from "./Readiness_Check.svelte"; +import Synchronize_Data from "./Synchronize_Data.svelte"; +import Authors_Upload from "./Authors_Upload.svelte"; +import Verify_And_Publish from "./Verify_And_Publish.svelte"; +import Waiting from "./Waiting.svelte"; +import Web_Verify from "./Web_Verify.svelte"; + +export const instructions: {[key: string]: typeof Waiting} = { + "asset_package_verify_and_publish": Asset_Package_Verify_And_Publish, + "app_configuration": App_Configuration, + // may need to add "author_app_configuration" later + // in S1 it has the same instructions as "app_configuration" + "create_app_entry": Create_App_Entry, + "authors_download": Authors_Download, + "googleplay_configuration": GooglePlay_Configuration, + "googleplay_verify_and_publish": GooglePlay_Verify_And_Publish, + "make_it_live": Make_It_Live, + "approval_pending": Approval_Pending, + "readiness_check": Readiness_Check, + "synchronize_data": Synchronize_Data, + "authors_upload": Authors_Upload, + "verify_and_publish": Verify_And_Publish, + "waiting": Waiting, + "web_verify": Web_Verify +}; \ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/+page.server.ts new file mode 100644 index 0000000000..444b3086d0 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/+page.server.ts @@ -0,0 +1,251 @@ +import { paginateSchema } from '$lib/table'; +import { isSuperAdmin } from '$lib/utils'; +import { idSchema } from '$lib/valibot'; +import type { Prisma } from '@prisma/client'; +import { error, redirect } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { RoleId } from 'sil.appbuilder.portal.common/prisma'; +import { superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; +import { minifyUser } from './common'; + +const lockSchema = v.object({ + user: idSchema, + active: v.boolean() +}); + +const userSchema = v.object({ + page: paginateSchema, + search: v.string(), + organizationId: v.nullable(idSchema) +}); + +function select(orgIds?: number[]) { + return { + Id: true, + Name: true, + Email: true, + IsLocked: true, + UserRoles: { + where: orgIds ? { OrganizationId: { in: orgIds } } : undefined, + select: { + RoleId: true, + OrganizationId: true + } + }, + GroupMemberships: { + where: orgIds ? { Group: { OwnerId: { in: orgIds } } } : undefined, + select: { + Group: { + select: { Id: true, OwnerId: true } + } + } + }, + OrganizationMemberships: { + where: orgIds ? { OrganizationId: { in: orgIds } } : undefined, + select: { + OrganizationId: true + } + } + }; +} + +// If we are a superadmin, collect all users, otherwise +// collect every user in one of our organizations +function adminOrDefaultWhere(isSuper: boolean, orgIds: number[]) { + return isSuper + ? { + // Get all users that are locked or are a member of at least one organization + // (Users that are not in an organization and are not locked are not interesting + // because they can't login and behave essentially as locked users or as users + // who have never logged in before) + OR: [ + { + OrganizationMemberships: { + some: {} + } + }, + { + IsLocked: true + } + ] + } + : { + OrganizationMemberships: { + some: { + OrganizationId: { in: orgIds } + } + } + }; +} + +export const load = (async (event) => { + const userInfo = (await event.locals.auth())?.user; + const userId = userInfo?.userId; + if (!userId) return redirect(302, '/'); + + const isSuper = isSuperAdmin(userInfo.roles); + + const organizations = await prisma.organizations.findMany({ + where: isSuper + ? undefined + : { + UserRoles: { + some: { + RoleId: RoleId.OrgAdmin, + UserId: userId + } + } + }, + select: { + Id: true, + Name: true, + UserRoles: true + } + }); + + const orgIds = organizations.map((o) => o.Id); + + const users = await prisma.users.findMany({ + orderBy: { + FamilyName: 'asc' + }, + select: select(isSuper ? undefined : orgIds), + where: adminOrDefaultWhere(isSuper, orgIds), + // More significantly, could paginate results to around 50 users/page = 10 KB + // Done - Aidan + take: 50 + }); + + return { + // Only pass essential information to the client. + // About 120 bytes per small user (with one organization and one group), and 240 for a larger user in several organizations. + // On average about 175 bytes per user. If PROD has 200 active users, that's 35 KB of data to send all of them. 620 total users = 110 KB + // The whole page is about 670 KB without this data. + // Only superadmins would see this larger size, most users have organizations with much fewer users where it does not matter + + users: users.map(minifyUser), + userCount: users.length, + // Could be improved by putting group names into a referenced palette + // (minimal returns if most users are in different organizations and groups) + // I went ahead and did this too. My assumption is that there will generally be multiple users in each organization and group, so I think this should be a justifiable change. - Aidan + groups: Object.fromEntries( + ( + await prisma.groups.findMany({ + where: { + OwnerId: { in: orgIds }, + GroupMemberships: { + some: {} + } + } + }) + ).map((g) => [g.Id, g.Name]) + ), + organizations: Object.fromEntries(organizations?.map((org) => [org.Id, org.Name])), + organizationCount: organizations.length, + form: await superValidate( + { + organizationId: organizations.length !== 1 ? null : organizations[0].Id, + page: { + page: 0, + size: 50 + } + }, + valibot(userSchema) + ) + }; +}) satisfies PageServerLoad; + +export const actions: Actions = { + async lock(event) { + const session = await event.locals.auth(); + if (!session) return error(403); + const form = await superValidate(event, valibot(lockSchema)); + if (!form.valid || session.user.userId === form.data.user) return { form, ok: false }; + await DatabaseWrites.users.update({ + where: { + Id: form.data.user + }, + data: { + IsLocked: !form.data.active + } + }); + return { form, ok: true }; + }, + + async page(event) { + const session = await event.locals.auth(); + if (!session) return error(403); + const form = await superValidate(event, valibot(userSchema)); + if (!form.valid) return { form, ok: false }; + + const isSuper = isSuperAdmin(session.user.roles); + + const organizations = await prisma.organizations.findMany({ + where: + form.data.organizationId !== null + ? { Id: form.data.organizationId } + : isSuper + ? undefined + : { + UserRoles: { + some: { + RoleId: RoleId.OrgAdmin, + UserId: session.user.userId + } + } + }, + select: { + Id: true, + Name: true, + UserRoles: true + } + }); + + const orgIds = organizations.map((o) => o.Id); + + const where: Prisma.UsersWhereInput = { + AND: [ + adminOrDefaultWhere(isSuper, orgIds), + { + OR: form.data.search + ? [ + { + Name: { + contains: form.data.search, + mode: 'insensitive' + } + }, + { + Email: { + contains: form.data.search, + mode: 'insensitive' + } + } + ] + : undefined + } + ] + }; + + const users = await prisma.users.findMany({ + orderBy: { + FamilyName: 'asc' + }, + select: select(isSuper ? undefined : orgIds), + where: where, + // More significantly, could paginate results to around 50 users/page = 10 KB + // Done - Aidan + skip: form.data.page.page * form.data.page.size, + take: form.data.page.size + }); + + return { + form, + ok: true, + query: { data: users.map(minifyUser), count: await prisma.users.count({ where: where }) } + }; + } +}; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/+page.svelte new file mode 100644 index 0000000000..aefa0be493 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/+page.svelte @@ -0,0 +1,177 @@ + + +
+
+
+

{m.users_title()}

+ {#if isAdmin(data.session?.user.roles)} + + + {m.organizationMembership_invite_create_inviteUserButtonTitle()} + + {/if} +
+
+ {#if data.organizationCount > 1} + + {/if} + + +
+
+ + + + + + + + + + + {#each users as user} + + + + + + + {/each} + +
{m.users_table_columns_name()}{m.users_table_columns_role()}{m.users_table_columns_groups()}{m.users_table_columns_active()}
+

+ + {user.N} + +

+

+ {user.E?.replace('@', '\u200b@')} +

+
+ {#each user.O as org} +
+ + + {data.organizations[org.I]} + + +
+ {org.R.map( + (r) => + [ + '', + m.users_roles_superAdmin(), + m.users_roles_orgAdmin(), + m.users_roles_appBuilder(), + m.users_roles_author() + ][r] + ).join(', ') || m.users_noRoles()} +
+ {/each} +
+ {#each user.O as org} +
+ + + {data.organizations[org.I]} + + +
+ {org.G.map((g) => data.groups[g]).join(', ') || m.common_none()} +
+ {/each} +
+
{ + return async ({ update }) => { + await update({ reset: false, invalidateAll: false }); + }; + }} + > + + { + if (data.session?.user.userId !== user.I) { + //@ts-ignore + e.currentTarget.parentElement?.requestSubmit(); + // Apparently full TS is not supported in this instance, otherwise I would just cast to HTMLFormElement + } + }} + /> +
+
+
+
+ + +
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/GroupsSelector.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/GroupsSelector.svelte new file mode 100644 index 0000000000..542039304d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/GroupsSelector.svelte @@ -0,0 +1,28 @@ + + +
+
+ {#each groups.sort((a, b) => sortByName(a, b, languageTag())) as group} +
+ + {group.Name} +
+ {/each} +
+
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/RolesSelector.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/RolesSelector.svelte new file mode 100644 index 0000000000..fcabc2baa5 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/RolesSelector.svelte @@ -0,0 +1,42 @@ + + +
+
+
+ + {m.users_roles_appBuilder()} +
+
+ + {m.users_roles_author()} +
+
+ + {m.users_roles_orgAdmin()} +
+
+
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/+layout.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/+layout.server.ts new file mode 100644 index 0000000000..b9c3d73c88 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/+layout.server.ts @@ -0,0 +1,21 @@ +import { isAdminForOrgs } from '$lib/utils'; +import { prisma } from 'sil.appbuilder.portal.common'; +import type { LayoutServerLoad } from './$types'; + +export const load = (async ({ params, locals }) => { + const subject = await prisma.users.findUnique({ + where: { + Id: parseInt(params.id ?? '') + }, + include: { + OrganizationMemberships: true + } + }); + const username = subject?.Name; + const roles = (await locals.auth())?.user.roles; + const canEdit = isAdminForOrgs( + subject?.OrganizationMemberships.map((mem) => mem.OrganizationId) ?? [], + roles + ); + return { username, canEdit }; +}) satisfies LayoutServerLoad; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/+layout.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/+layout.svelte new file mode 100644 index 0000000000..cdd1de9448 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/+layout.svelte @@ -0,0 +1,36 @@ + + + +{#if data.canEdit} + + {@render children?.()} + +{:else} +

{m.profile_title()}: {data.username}

+ {@render children?.()} +{/if} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/+page.server.ts new file mode 100644 index 0000000000..df476402d4 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/+page.server.ts @@ -0,0 +1,6 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load = (async ({ params: { id } }) => { + return redirect(302, '/users/' + id + '/settings/profile'); +}) satisfies PageServerLoad; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/groups/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/groups/+page.server.ts new file mode 100644 index 0000000000..e1137cadc2 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/groups/+page.server.ts @@ -0,0 +1,105 @@ +import { isAdminForOrg, isSuperAdmin } from '$lib/utils'; +import { idSchema } from '$lib/valibot'; +import { error } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { RoleId } from 'sil.appbuilder.portal.common/prisma'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const groupsSchema = v.object({ + organizations: v.array( + v.object({ + name: v.nullable(v.string()), + id: idSchema, + groups: v.array(idSchema) + // enabled: v.boolean() + }) + ) +}); + +export const load = (async ({ params, locals }) => { + const userData = (await locals.auth())?.user; + const userId = userData?.userId; + const isSuper = isSuperAdmin(userData?.roles); + const subjectUserId = parseInt(params.id); + + const groupsByOrg = await prisma.organizations.findMany({ + where: { + Owner: { + // Only send a list of groups for orgs that the subject user is in and the current user has access to + AND: isSuper + ? undefined + : { + UserRoles: { + some: { + UserId: userId, + RoleId: RoleId.OrgAdmin + } + } + }, + OrganizationMemberships: { + some: { + UserId: subjectUserId + } + } + } + }, + select: { + Id: true, + Name: true, + Groups: true + } + }); + + const groupMemberships = ( + await prisma.groupMemberships.findMany({ + where: { + UserId: subjectUserId + }, + select: { + GroupId: true + } + }) + ).map((g) => g.GroupId); + // If there are no groups the current user has admin access to return Forbidden + if (groupsByOrg.length === 0) return error(403); + const form = await superValidate( + { + organizations: groupsByOrg.map((o) => ({ + id: o.Id, + name: o.Name, + groups: o.Groups.filter((g) => groupMemberships.includes(g.Id)).map((g) => g.Id) + })) + }, + valibot(groupsSchema) + ); + return { + form, + groupsByOrg + }; +}) satisfies PageServerLoad; + +export const actions = { + async default(event) { + // TODO: test this action + // TODO: I really want to change all many-to-many relationships in db to have composite primary keys + // In this case that would be GroupMemberships PRIMARY KEY(GroupId, UserId) + // This way they can be added and removed in constant time and in a single command + // https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations + + const form = await superValidate(event, valibot(groupsSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + + const user = (await event.locals.auth())!.user; + + // Filter for legal orgs to modify, then map to relevant table entries + const newRelationEntries = form.data.organizations + .filter((org) => isAdminForOrg(org.id, user.roles)) + .flatMap((org) => org.groups.map((group) => group)); + const uId = parseInt(event.params.id); + const success = await DatabaseWrites.groupMemberships.updateUserGroups(uId, newRelationEntries); + return { form, ok: success }; + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/groups/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/groups/+page.svelte new file mode 100644 index 0000000000..67b4c25217 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/groups/+page.svelte @@ -0,0 +1,31 @@ + + +
+
+ {#each $form.organizations as org} + {@const groups = data.groupsByOrg.find((o) => o.Id === org.id)?.Groups ?? []} +

{org.name}

+ + + + {/each} +
+ +
+
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/profile/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/profile/+page.server.ts new file mode 100644 index 0000000000..fe7d0e2b81 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/profile/+page.server.ts @@ -0,0 +1,118 @@ +import { isAdminForOrgs } from '$lib/utils'; +import { idSchema } from '$lib/valibot'; +import type { Session } from '@auth/sveltekit'; +import { error } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const profileSchema = v.object({ + id: idSchema, + firstName: v.string(), + lastName: v.string(), + displayName: v.string(), + email: v.pipe(v.string(), v.email()), + // Legal phone numbers: (123) 456-7890 1234567890 123-4567890 123 456-7890 + phone: v.pipe(v.string(), v.regex(/[\d-() ]+/)), + timezone: v.string(), // ? + notifications: v.boolean(), + visible: v.boolean(), + active: v.boolean() +}); + +function verifyAllowed( + currentUser: Session['user'], + subjectUserId: number, + subjectOrganizations: number[] +) { + // If we are not editing ourselves and we are not an admin who can edit this user, block + return ( + currentUser.userId === subjectUserId || isAdminForOrgs(subjectOrganizations, currentUser.roles) + ); +} + +export const load = (async ({ params, locals, parent }) => { + const user = (await locals.auth())?.user; + const subject = await prisma.users.findUnique({ + where: { + Id: parseInt(params.id) + }, + include: { + OrganizationMemberships: true + } + }); + if (!subject) return error(404); + if ( + !verifyAllowed( + user!, + parseInt(params.id), + subject.OrganizationMemberships.map((mem) => mem.OrganizationId) + ) + ) { + return error(403); + } + const form = await superValidate( + { + id: subject.Id, + firstName: subject.GivenName ?? '', + lastName: subject.FamilyName ?? '', + displayName: subject.Name ?? '', + email: subject.Email ?? '', + phone: subject.Phone ?? '', + timezone: subject.Timezone ?? '', + notifications: subject.EmailNotification ?? false, + visible: !!subject.ProfileVisibility, + active: !subject.IsLocked + }, + valibot(profileSchema) + ); + return { form }; +}) satisfies PageServerLoad; + +export const actions = { + async default(event) { + const form = await superValidate(event, valibot(profileSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + const user = (await event.locals.auth())!.user; + if (form.data.id !== parseInt(event.params.id)) return fail(400); + const subject = await prisma.users.findUnique({ + where: { + Id: form.data.id + }, + include: { + OrganizationMemberships: true + } + }); + if (!subject) return error(404); + if ( + !verifyAllowed( + user, + form.data.id, + subject.OrganizationMemberships.map((mem) => mem.OrganizationId) + ) + ) { + return fail(403); + } + await DatabaseWrites.users.update({ + where: { + Id: form.data.id + }, + data: { + GivenName: form.data.firstName, + FamilyName: form.data.lastName, + Name: form.data.displayName, + Email: form.data.email, + Phone: form.data.phone, + // TODO: sync user data with dwkit + Timezone: form.data.timezone, + EmailNotification: form.data.notifications, + ProfileVisibility: form.data.visible ? 1 : 0, + // You cannot change lock state of yourself, and if you are editing someone else, you are either org admin or superadmin + IsLocked: form.data.id === user.userId ? undefined : !form.data.active + } + }); + return { form, ok: true }; + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/profile/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/profile/+page.svelte new file mode 100644 index 0000000000..12e34e375a --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/profile/+page.svelte @@ -0,0 +1,164 @@ + + +
+
+ + + + + + + + + + + + + + + + + fuzzySearch.search(search).slice(0, 7)} + onItemClicked={(item) => { + // Not strictly necessary because it will be set onSubmit but here anyways + // @ts-expect-error the property key should always exist in our use case + $form.timezone = item.key; + // @ts-expect-error the property value should always exist in our use case + tzValue = item.value; + }} + bind:search={tzValue} + classes="w-full {!tzValue || timeZoneMap.has(tzValue) ? '' : 'select-error'}" + dropdownClasses="w-full" + > + {#snippet listElement({ item })} +
+ {item.item.value} +
+ {/snippet} +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/roles/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/roles/+page.server.ts new file mode 100644 index 0000000000..d3fb0d1a50 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/roles/+page.server.ts @@ -0,0 +1,82 @@ +import { isAdminForOrg, isSuperAdmin, orgsForRole } from '$lib/utils'; +import { idSchema } from '$lib/valibot'; +import { error } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { RoleId } from 'sil.appbuilder.portal.common/prisma'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const rolesSchema = v.object({ + organizations: v.array( + v.object({ + name: v.nullable(v.string()), + id: idSchema, + roles: v.array(idSchema) + // enabled: v.boolean() + }) + ) +}); + +export const load = (async ({ locals, params }) => { + const auth = (await locals.auth())?.user.roles; + const isSuper = isSuperAdmin(auth); + const orgs = orgsForRole(RoleId.OrgAdmin, auth); + if (!(isSuper || orgs?.length)) return error(403); + const subjectId = parseInt(params.id); + + const rolesByOrg = await prisma.organizations.findMany({ + where: { + OrganizationMemberships: { + some: { + UserId: subjectId + } + }, + Id: isSuper ? undefined : { in: orgs } + }, + select: { + Id: true, + Name: true, + UserRoles: { + where: { + UserId: subjectId + } + } + } + }); + + // return 404 if user doesn't have any memberships/doesn't exist + if (!rolesByOrg.length) return error(404); + + const form = await superValidate( + { + organizations: rolesByOrg.map((o) => ({ + id: o.Id, + name: o.Name, + roles: o.UserRoles.map((ur) => ur.RoleId) + })) + }, + valibot(rolesSchema) + ); + return { form }; +}) satisfies PageServerLoad; + +export const actions = { + async default(event) { + const form = await superValidate(event, valibot(rolesSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + + const user = (await event.locals.auth())!.user; + + // Filter for legal orgs to modify, then map to relevant table entries + const newRelationEntries = form.data.organizations.filter((org) => + isAdminForOrg(org.id, user.roles) + ); + const subjectUserId = parseInt(event.params.id); + for (const org of newRelationEntries) { + await DatabaseWrites.userRoles.setUserRolesForOrganization(subjectUserId, org.id, org.roles); + } + return { form, ok: true }; + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/roles/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/roles/+page.svelte new file mode 100644 index 0000000000..c14d4b1541 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/[id=idNumber]/settings/roles/+page.svelte @@ -0,0 +1,30 @@ + + +
+
+ {#each $form.organizations as org} +

{org.name}

+ + + + {/each} +
+ +
+
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/common.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/common.ts new file mode 100644 index 0000000000..f2d296351c --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/common.ts @@ -0,0 +1,41 @@ +type UserInfo = { + Id: number; + Name: string | null; + Email: string | null; + UserRoles: { OrganizationId: number; RoleId: number }[]; + GroupMemberships: { Group: { Id: number; OwnerId: number } }[]; + OrganizationMemberships: { + OrganizationId: number; + }[]; + IsLocked: boolean; +}; + +// or by using smaller (or even minified) keys (eg N instead of Name, O instead of Organizations) +// Done - Aidan +export function minifyUser(user: UserInfo) { + return { + /** User Id */ + I: user.Id, + /** User Name */ + N: user.Name, + /** User Email */ + E: user.Email, + /** User OrganizationMemberships */ + O: user.OrganizationMemberships.map((orgMem) => ({ + /** Roles */ + R: user.UserRoles.filter((ur) => ur.OrganizationId === orgMem.OrganizationId).map( + (r) => r.RoleId + ), + /** Organization Id */ + I: orgMem.OrganizationId, + /** Group Ids */ + G: user.GroupMemberships.filter( + (groupMem) => groupMem.Group.OwnerId === orgMem.OrganizationId + ).map((group) => group.Group.Id) + })), + /** User IsLocked */ + A: !user.IsLocked + }; +} + +export type MinifiedUser = ReturnType; \ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/invite/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/invite/+page.server.ts new file mode 100644 index 0000000000..ebe9760119 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/invite/+page.server.ts @@ -0,0 +1,69 @@ +import { isAdminForOrg, isSuperAdmin } from '$lib/utils'; +import { idSchema } from '$lib/valibot'; +import { fail } from '@sveltejs/kit'; +import { DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { RoleId } from 'sil.appbuilder.portal.common/prisma'; +import { superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const createSchema = v.object({ + email: v.pipe(v.string(), v.email()), + organizationId: idSchema, + roles: v.array(idSchema), + groups: v.array(idSchema) +}); + +export const load = (async ({ locals }) => { + const form = await superValidate(valibot(createSchema)); + + const user = await locals.auth(); + const groupsByOrg = await prisma.organizations.findMany({ + where: { + // Only send a list of groups for orgs that the subject user is in and the current user has access to + UserRoles: isSuperAdmin(user?.user.roles) + ? undefined + : { + some: { + UserId: user?.user.userId, + RoleId: RoleId.OrgAdmin + } + } + }, + select: { + Id: true, + Name: true, + Groups: true + } + }); + return { form, groupsByOrg }; +}) satisfies PageServerLoad; + +export const actions = { + async new({ request, locals, url }) { + const form = await superValidate(request, valibot(createSchema)); + if (!form.valid) { + return fail(400, { form, ok: false, errors: form.errors }); + } + const user = await locals.auth(); + if (!user || !isAdminForOrg(form.data.organizationId, user.user.roles)) return fail(401); + try { + const { email, organizationId, roles, groups } = form.data; + const inviteToken = await DatabaseWrites.organizationMemberships.createOrganizationInvite( + email, + organizationId, + user.user.userId, + roles, + groups + ); + const inviteLink = `${url.origin}/invitations/organization-membership?t=${inviteToken}`; + // TODO: send email- log instead + console.log(inviteLink, email); + return { ok: true, form }; + } catch (e) { + if (e instanceof v.ValiError) return { form, ok: false, errors: e.issues }; + throw e; + } + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/invite/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/invite/+page.svelte new file mode 100644 index 0000000000..ba2f2747a9 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/users/invite/+page.svelte @@ -0,0 +1,107 @@ + + +
+

{m.organizationMembership_invite_create_inviteUserModalTitle()}

+ +
+
+
+ + + + + + + +
+
+ + Assigned Roles and Groups +
+
+
+ {m.users_userRoles()} + +
+
+ {m.users_userGroups()} + +
+
+
+
+
+ + {#if $allErrors.length} +
    + {#each $allErrors as error} +
  • + {error.path}: + {error.messages.join('. ')} +
  • + {/each} +
+ {/if} + +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/workflow-instances/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/workflow-instances/+page.server.ts new file mode 100644 index 0000000000..ea22237c1c --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/workflow-instances/+page.server.ts @@ -0,0 +1,197 @@ +import { paginateSchema } from '$lib/table'; +import { isSuperAdmin } from '$lib/utils'; +import { idSchema } from '$lib/valibot'; +import type { Prisma } from '@prisma/client'; +import { error } from '@sveltejs/kit'; +import { prisma } from 'sil.appbuilder.portal.common'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const tableSchema = v.object({ + page: paginateSchema, + sort: v.nullable( + v.object({ + field: v.string(), + direction: v.picklist(['asc', 'desc']) + }) + ), + search: v.string(), + productDefinitionId: v.nullable(idSchema), + dateUpdatedRange: v.nullable(v.tuple([v.date(), v.nullable(v.date())])), + organizationId: v.nullable(idSchema) +}); + +export const load: PageServerLoad = async (event) => { + const instances = await prisma.workflowInstances.findMany({ + select: { + State: true, + DateUpdated: true, + Product: { + select: { + Id: true, + ProductDefinition: { + select: { + Name: true + } + }, + Project: { + select: { + Id: true, + Name: true, + Organization: { + select: { + Id: true, + Name: true + } + } + } + } + } + } + }, + take: 50 + }); + + return { + instances, + count: await prisma.workflowInstances.count(), + form: await superValidate( + { + page: { + page: 0, + size: 50 + } + }, + valibot(tableSchema) + ), + productDefinitions: await prisma.productDefinitions.findMany({ + select: { + Id: true, + Name: true + } + }) + }; +}; + +export const actions: Actions = { + page: async function ({ request, locals }) { + const session = await locals.auth(); + if (!isSuperAdmin(session?.user.roles)) return error(403); + const form = await superValidate(request, valibot(tableSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + + const where: Prisma.WorkflowInstancesWhereInput = { + OR: form.data.search + ? [ + { + Product: { + Project: { + Organization: { + Name: { + contains: form.data.search, + mode: 'insensitive' + } + } + } + } + }, + { + Product: { + Project: { + Name: { + contains: form.data.search, + mode: 'insensitive' + } + } + } + }, + { + Product: { + ProductDefinition: { + Name: { + contains: form.data.search, + mode: 'insensitive' + } + } + } + }, + { + State: { + contains: form.data.search, + mode: 'insensitive' + } + } + ] + : undefined, + Product: form.data.productDefinitionId + ? { + ProductDefinitionId: form.data.productDefinitionId + } + : undefined, + DateUpdated: + form.data.dateUpdatedRange && form.data.dateUpdatedRange[1] + ? { + gt: form.data.dateUpdatedRange[0], + lt: form.data.dateUpdatedRange[1] + } + : undefined + }; + + const instances = await prisma.workflowInstances.findMany({ + orderBy: + form.data.sort?.field === 'product' + ? { ProductId: form.data.sort.direction } + : form.data.sort?.field === 'organization' + ? { Product: { Project: { Organization: { Name: form.data.sort.direction } } } } + : form.data.sort?.field === 'project' + ? { Product: { Project: { Name: form.data.sort.direction } } } + : form.data.sort?.field === 'definition' + ? { Product: { ProductDefinition: { Name: form.data.sort.direction } } } + : form.data.sort?.field === 'state' + ? { State: form.data.sort.direction } + : form.data.sort?.field === 'date' + ? { DateUpdated: form.data.sort.direction } + : undefined, + where: where, + select: { + State: true, + DateUpdated: true, + Product: { + select: { + Id: true, + ProductDefinition: { + select: { + Name: true + } + }, + Project: { + select: { + Id: true, + Name: true, + Organization: { + select: { + Id: true, + Name: true + } + } + } + } + } + } + }, + skip: form.data.page.page * form.data.page.size, + take: form.data.page.size + }); + + return { + form, + ok: true, + query: { + data: instances, + count: await prisma.workflowInstances.count({ where: where }) + } + }; + } +}; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/workflow-instances/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/workflow-instances/+page.svelte new file mode 100644 index 0000000000..2a5b5e1e0f --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/workflow-instances/+page.svelte @@ -0,0 +1,157 @@ + + +
+ +
{ + if (event.key === 'Enter') submit(); + }} + > +
+
+

{m.workflowInstances_title()}

+
+
+ + +
+
+
+ + +
+
+
+ {#if instances.length > 0} + i.Product.Project.Organization, + render: (o) => `${o.Name}`, + sortable: true + }, + { + id: 'project', + header: m.project_title(), + data: (i) => i.Product.Project, + render: (c) => `${c.Name}`, + sortable: true + }, + { + id: 'product', + header: m.tasks_product(), + data: (i) => i.Product, + render: (c) => + `${c.ProductDefinition.Name}`, + sortable: true + }, + { + id: 'state', + header: m.project_products_transitions_state(), + data: (i) => i.State, + sortable: true + }, + { + id: 'date', + header: m.common_updated(), + data: (i) => i.DateUpdated, + // TODO: tooltip will need to wait until Svelte 5 + render: (c) => `${getRelativeTime(c)}`, + sortable: true + } + ]} + serverSide={true} + className="max-h-full" + onSort={(field, direction) => + form.update((data) => ({ ...data, sort: { field, direction } }))} + /> + {:else} +

{m.workflowInstances_empty()}

+ {/if} +
+ +
{ + if (event.key === 'Enter') submit(); + }} + > +
+ +
+
+
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/workflow-instances/[product_id]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/workflow-instances/[product_id]/+page.server.ts new file mode 100644 index 0000000000..5b1904ac5b --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/workflow-instances/[product_id]/+page.server.ts @@ -0,0 +1,120 @@ +import { isSuperAdmin } from '$lib/utils'; +import { error } from '@sveltejs/kit'; +import { prisma, Workflow } from 'sil.appbuilder.portal.common'; +import { WorkflowAction, type WorkflowState } from 'sil.appbuilder.portal.common/workflow'; +import { fail, superValidate } from 'sveltekit-superforms'; +import { valibot } from 'sveltekit-superforms/adapters'; +import * as v from 'valibot'; +import type { Actions, PageServerLoad } from './$types'; + +const jumpStateSchema = v.object({ + state: v.string() +}); + +export const load: PageServerLoad = async ({ params }) => { + // route already protected by hooks.server.ts + const product = await prisma.products.findUnique({ + where: { + Id: params.product_id + }, + select: { + Id: true, + Store: { + select: { + Description: true + } + }, + Project: { + select: { + Id: true, + Name: true, + Organization: { + select: { + Id: true, + Name: true + } + } + } + }, + ProductDefinition: { + select: { + Name: true + } + }, + ProductTransitions: { + select: { + DateTransition: true, + DestinationState: true, + InitialState: true, + Command: true, + TransitionType: true, + WorkflowType: true, + AllowedUserNames: true, + Comment: true + }, + orderBy: [ + { + DateTransition: 'asc' + } + ] + } + } + }); + + if (!product) return error(404); + + const flow = await Workflow.restore(params.product_id); + + const snap = await Workflow.getSnapshot(params.product_id); + if (!(flow && snap)) return error(404); + + const workflowDefinition = await prisma.workflowDefinitions.findUnique({ + where: { + Id: snap.definitionId + }, + select: { + Name: true + } + }); + + return { + product: { + ...product, + Transitions: product?.ProductTransitions, + ProductTransitions: undefined + }, + snapshot: snap, + machine: snap ? flow.serializeForVisualization() : [], + definition: workflowDefinition, + form: await superValidate( + { + state: snap?.state + }, + valibot(jumpStateSchema) + ) + }; +}; + +export const actions = { + default: async ({ request, params, locals }) => { + if (!isSuperAdmin((await locals.auth())?.user.roles)) { + return error(403); + } + + const form = await superValidate(request, valibot(jumpStateSchema)); + if (!form.valid) return fail(400, { form, ok: false }); + + const flow = await Workflow.restore(params.product_id); + + if (!flow) return fail(404, { form, ok: false }); + + // TODO: What if the parent project is archived? This will create user tasks, which we probably don't want. + flow.send({ + type: WorkflowAction.Jump, + target: form.data.state as WorkflowState, + userId: null + }); + + return { form, ok: true }; + } +} satisfies Actions; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/workflow-instances/[product_id]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/workflow-instances/[product_id]/+page.svelte new file mode 100644 index 0000000000..39b6c149e6 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/workflow-instances/[product_id]/+page.svelte @@ -0,0 +1,276 @@ + + +
+ + +
+ {#if ready} + + {#each data.machine as state, i} + + + + { + $form.state = state.label; + }} + > + + + {state.label} + + + {#each state.connections as conn} + + {/each} + {#each { length: state.inCount } as c} + + {/each} + + {/each} + + {:else} + + {/if} +
+
+ + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/+layout.svelte b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/+layout.svelte new file mode 100644 index 0000000000..500ed9518a --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/+layout.svelte @@ -0,0 +1,12 @@ + + +
+
+ {@render children?.()} +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/login/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/login/+page.server.ts new file mode 100644 index 0000000000..40bc9d139a --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/login/+page.server.ts @@ -0,0 +1,14 @@ +import { redirect } from '@sveltejs/kit'; +import { signIn } from '../../../../auth'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async (event) => { + if ((await event.locals.auth())?.user) { + if (event.cookies.get('inviteToken')) { + const inviteToken = event.cookies.get('inviteToken')!; + return redirect(302, '/invitations/organization-membership?t=' + inviteToken); + } + return redirect(302, '/tasks'); + } +}; +export const actions: Actions = { default: signIn }; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/login/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/login/+page.svelte new file mode 100644 index 0000000000..ca81ce563c --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/login/+page.svelte @@ -0,0 +1,44 @@ + + +
+
+
+ +
+

{m.welcome()}

+
+ + {#snippet submitButton()} + +
Sign In
+ {/snippet} +
+ + {#snippet submitButton()} +
Sign In with new session
+ {/snippet} +
+
+
+
+ + Auth0 badge + + diff --git a/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/login/locked/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/login/locked/+page.svelte new file mode 100644 index 0000000000..c47606f46d --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/login/locked/+page.svelte @@ -0,0 +1,8 @@ + + +
+

{m.errors_notactiveUser()}

+

{@html m.errors_notactiveUserText()}

+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/login/no-organization/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/login/no-organization/+page.svelte new file mode 100644 index 0000000000..480c0fe232 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/login/no-organization/+page.svelte @@ -0,0 +1,8 @@ + + +
+

{m.errors_orgMembershipRequired()}

+

{@html m.errors_orgMembershipRequiredText()}

+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/logout/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/logout/+page.server.ts new file mode 100644 index 0000000000..36d964eb81 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/logout/+page.server.ts @@ -0,0 +1,8 @@ +import { redirect } from '@sveltejs/kit'; +import { signOut } from '../../../../auth'; +import type { Actions, PageServerLoad } from './$types'; + +export const load: PageServerLoad = async (event) => { + if (!(await event.locals.auth())?.user) return redirect(302, '/'); +}; +export const actions: Actions = { default: signOut }; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/logout/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/logout/+page.svelte new file mode 100644 index 0000000000..912e1ef5f0 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/(auth)/logout/+page.svelte @@ -0,0 +1,13 @@ + + +
+

{m.header_signOut()} and return to home page

+ + {#snippet submitButton()} +
{m.header_signOut()}
+ {/snippet} +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/api/products/[package]/published/+server.ts b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/api/products/[package]/published/+server.ts new file mode 100644 index 0000000000..ac33700850 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/api/products/[package]/published/+server.ts @@ -0,0 +1,113 @@ +import { getFileInfo } from '$lib/products'; +import { getPublishedFile } from '$lib/products/server'; +import { prisma } from 'sil.appbuilder.portal.common'; + +type ManifestResponse = { + url: string; + icon: string; + color: string; + ['default-language']: string; + ['download-apk-strings']: Record; + languages: string[]; + files: string[]; +}; + +export async function GET({ params }) { + // Get the play-listing/manifest.json artifact + const manifestArtifact = await getPublishedAppDetails(params.package); + if (!manifestArtifact?.Url) { + return new Response(null, { status: 404 }); + } + // Get the size of the apk + const apkArtifact = await getPublishedFile(manifestArtifact.ProductId, 'apk'); + if (!apkArtifact?.Url) { + return new Response(null, { status: 404 }); + } + const { fileSize } = await getFileInfo(apkArtifact.Url); + + // Get the contents of the manifest.json + const manifestJson = await fetch(manifestArtifact.Url).then((r) => r.text()); + + const manifest = JSON.parse(manifestJson) as ManifestResponse; + + // The bucket in the URL stored in the manifest can change over time. The URL from + // the artifact query is updated when buckets change. Update the hostname stored + // in the manifest file based on the hostname from the artifact query. + const manifestUri = new URL(manifestArtifact.Url); + const url = new URL(manifest.url); + url.host = manifestUri.host; + const titles = {} as Record; + const descriptions = {} as Record; + for (const language of manifest.languages) { + let title = ''; + const titleSearch = new RegExp(`${language}/title.txt`); + const titlePath = manifest.files.find((s) => titleSearch.test(s)); + + if (titlePath) { + title = await fetch(url + titlePath).then((r) => r.text()); + } + titles[language] = title.trim(); + + let description = ''; + const descriptionSearch = new RegExp(`${language}/short_description.txt`); + const descriptionPath = manifest.files.find((s) => descriptionSearch.test(s)); + if (descriptionPath) { + description = await fetch(url + descriptionPath).then((r) => r.text()); + } + descriptions[language] = description; + } + + const details = { + ...manifest, + id: manifestArtifact.ProductId, + link: `/api/products/${manifestArtifact.ProductId}/files/published/apk`, + size: fileSize, + icon: url + manifest.icon, + titles: titles, + descriptions: descriptions, + url: undefined, + files: undefined + }; + + return new Response(JSON.stringify(details), { status: 200 }); +} + +async function getPublishedAppDetails(Package: string) { + const publications = await prisma.productPublications.findMany({ + where: { + Package, + Success: true + }, + include: { + ProductBuild: { + include: { + ProductArtifacts: { + select: { + ProductId: true, + ArtifactType: true, + Url: true + } + } + } + } + }, + orderBy: { + Id: 'asc' + } + }); + + for (const publication of publications) { + if (!publication.ProductBuild.ProductArtifacts.length) { + continue; + } + const artifact = publication.ProductBuild.ProductArtifacts.find( + (pa) => pa.ArtifactType === 'play-listing-manifest' + ); + + if (artifact) { + return artifact; + } + } + + return null; +} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/api/products/[product_id]/files/published/[type]/+server.ts b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/api/products/[product_id]/files/published/[type]/+server.ts new file mode 100644 index 0000000000..b661febe88 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/api/products/[product_id]/files/published/[type]/+server.ts @@ -0,0 +1,42 @@ +import { getFileInfo } from '$lib/products'; +import { getPublishedFile } from '$lib/products/server'; +import { error, redirect } from '@sveltejs/kit'; + +export async function GET({ params }) { + const productArtifact = await getPublishedFile(params.product_id, params.type); + if (!productArtifact?.Url) { + return error(404); + } + return redirect(302, productArtifact.Url); +} + +export async function HEAD({ params, request }) { + const ifModifiedSince = request.headers.get('If-Modified-Since') ?? ''; + + const productArtifact = await getPublishedFile(params.product_id, params.type); + if (!productArtifact?.Url) { + return error(404); + } + + const { lastModified, fileSize } = await getFileInfo(productArtifact.Url); + + const headers: Record = { + 'Last-Modified': lastModified + } + + if (fileSize) { + headers['Content-Length'] = fileSize; + } + + if (ifModifiedSince === lastModified) { + return new Response(null, { + status: 304, + headers + }); + } + + return new Response(null, { + status: 200, + headers + }); +} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/api/projects/[id=idNumber]/token/+server.ts b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/api/projects/[id=idNumber]/token/+server.ts new file mode 100644 index 0000000000..6265be35e5 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/api/projects/[id=idNumber]/token/+server.ts @@ -0,0 +1,246 @@ +import { isAdmin } from '$lib/utils.js'; +import { error, json } from '@sveltejs/kit'; +import { jwtVerify } from 'jose'; +import { createPublicKey } from 'node:crypto'; +import { BuildEngine, DatabaseWrites, prisma } from 'sil.appbuilder.portal.common'; +import { ProductTransitionType } from 'sil.appbuilder.portal.common/prisma'; + +const TOKEN_USE_HEADER = 'Use'; +const TOKEN_USE_UPLOAD = 'Upload'; +const TOKEN_USE_DOWNLOAD = 'Download'; + +export async function POST({ params, request, fetch }) { + if (!request.headers.get('Authorization')) { + return error(401, `Unauthorized`); + } + + const authToken = request.headers.get('Authorization')?.replace('Bearer ', ''); + + let jwtData; + try { + jwtData = await decryptJwtWithAuth0(authToken ?? ''); + } catch (e) { + // Signature verification failed + return error(401, `Unauthorized`); + } + const user = await prisma.users.findMany({ + where: { + ExternalId: jwtData.payload.sub + }, + select: { + Id: true, + Name: true, + Email: true, + WorkflowUserId: true, + UserRoles: { + select: { + OrganizationId: true, + RoleId: true + } + } + } + }); + if (user.length !== 1) { + // Should never happen + // (unless the user doesn't exist?) + return error(500, `Internal Server Error`); + } + + // user[0] is now authenticated. Still need to check authorization + const projectId = parseInt(params.id); + + const tokenUse = request.headers.get(TOKEN_USE_HEADER); + + const project = await prisma.projects.findUnique({ + where: { + Id: projectId + }, + select: { + WorkflowProjectUrl: true, + WorkflowProjectId: true, + Owner: { + select: { + Id: true, + ExternalId: true + } + }, + OrganizationId: true + } + }); + if (!project) { + return new Response( + JSON.stringify({ + errors: [{ title: `Project id=${projectId} not found` }] + }), + { + status: 404 + } + ); + } + + if (!project.WorkflowProjectUrl) { + return new Response( + JSON.stringify({ + errors: [{ title: `Project id=${projectId}: WorkflowProjectUrl is null` }] + }), + { + status: 404 + } + ); + } + + // Check ownership + let readOnly: boolean | null = null; + if (user[0].Id === project.Owner.Id) { + readOnly = false; + } + + // Check roles + if (readOnly === null) { + if (isAdmin(user[0].UserRoles.map((ur) => [ur.RoleId, ur.OrganizationId]))) { + readOnly = true; + } + } + + // Check authors + if (readOnly === null) { + const authors = await prisma.authors.findMany({ + where: { + ProjectId: projectId + }, + select: { + UserId: true + } + }); + if (authors.find((a) => a.UserId === user[0].Id)) { + // TODO: Kalaam now wants authors to be able to update at any time. In the future, we can add a setting on the author to whether they are a restricted author or not. I don't have time to add the UI at the moment. + //readOnly = !author.CanUpdate; + readOnly = false; + } + } + + if (readOnly === null) { + return new Response( + JSON.stringify({ + errors: [ + { + title: `Project id=${projectId}, user='${user[0].Name}' with email='${user[0].Email}' does not have permission to access` + } + ] + }), + { + status: 403 + } + ); + } + + if (tokenUse && tokenUse === TOKEN_USE_UPLOAD && readOnly) { + return new Response( + JSON.stringify({ + errors: [ + { + title: `Project id=${projectId}, user='${user[0].Name}' with email='${user[0].Email}' does not have permission to Upload` + } + ] + }), + { + status: 403 + } + ); + } + + const tokenResult = await BuildEngine.Requests.getProjectAccessToken( + { type: 'query', organizationId: project.OrganizationId }, + project.WorkflowProjectId, + { + name: project.Owner.ExternalId ?? '', + ReadOnly: readOnly + } + ); + + if (!tokenResult || tokenResult.responseType === 'error') { + return new Response( + JSON.stringify({ + errors: [{ title: `Project id=${projectId}: GetProjectToken returned null` }] + }), + { + status: 400 + } + ); + } + if (tokenResult.SecretAccessKey == null) { + return new Response( + JSON.stringify({ + errors: [{ title: `Project id=${projectId}: Token.SecretAccessKey is null` }] + }), + { + status: 400 + } + ); + } + const projectToken = { + type: 'project-tokens', + attributes: { + id: projectId, + url: project.WorkflowProjectUrl, + 'session-token': tokenResult.SessionToken, + 'secret-access-key': tokenResult.SecretAccessKey, + 'access-key-id': tokenResult.AccessKeyId, + expiration: tokenResult.Expiration, + region: tokenResult.Region, + 'read-only': tokenResult.ReadOnly + } + }; + + let use = readOnly ? 'ReadOnly Access' : 'ReadWrite Access'; + + if (tokenUse) { + use = tokenUse; + } + const products = await prisma.products.findMany({ + where: { ProjectId: projectId }, + select: { Id: true } + }); + + await DatabaseWrites.productTransitions.createMany({ + data: products.map((p) => ({ + ProductId: p.Id, + AllowedUserNames: user[0].Name, + TransitionType: ProductTransitionType.ProjectAccess, + InitialState: 'Project ' + use, + WorkflowUserId: user[0].WorkflowUserId, + DateTransition: new Date() + })) + }); + + return json({ data: projectToken }); +} +const secrets = (async () => { + const res = await fetch( + 'https://' + import.meta.env.VITE_AUTH0_DOMAIN + '/.well-known/jwks.json' + ); + const keys: { + kty: string; + use: string; + n: string; + e: string; + kid: string; + x5t: string; + x5c: string[]; + alg: string; + }[] = (await res.json()).keys; + return new Map( + keys.map((key) => [ + key.kid, + createPublicKey({ + key, + format: 'jwk' + }) + ]) + ); +})(); +async function decryptJwtWithAuth0(jwt: string) { + return jwtVerify(jwt, async (header, token) => { + return (await secrets).get(header.kid!)!; + }); +} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/invitations/organization-membership/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/invitations/organization-membership/+page.server.ts new file mode 100644 index 0000000000..a368de5622 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/invitations/organization-membership/+page.server.ts @@ -0,0 +1,23 @@ +import { acceptOrganizationInvite, checkInviteErrors } from '$lib/organizationInvites'; +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load = (async (event) => { + const inviteToken = event.url.searchParams.get('t')!; + // Clear the inviteToken cookie if it exists + if (event.cookies.get('inviteToken')) { + event.cookies.set('inviteToken', '', { path: '/' }); + } + const errors = await checkInviteErrors(inviteToken); + if (errors.error) return errors; + const session = await event.locals.auth(); + if (session) { + return await acceptOrganizationInvite(session.user.userId, inviteToken); + } else { + // Add a session scope cookie to redirect to the invite page after login + event.cookies.set('inviteToken', inviteToken, { + path: '/' + }); + return redirect(302, '/login'); + } +}) satisfies PageServerLoad; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/invitations/organization-membership/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/invitations/organization-membership/+page.svelte new file mode 100644 index 0000000000..1579ac7f87 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(unauthenticated)/invitations/organization-membership/+page.svelte @@ -0,0 +1,44 @@ + + +
+
+ {#if data.error === 'not found'} +

{m.organizationMembership_invite_error_notFound()}

+ {:else if data.error === 'redeemed'} +

{m.organizationMembership_invite_error_redeemed()}

+ {:else if data.error === 'expired'} +

{m.organizationMembership_invite_error_expired()}

+ {:else} +

{m.organizationMembership_invite_redemptionTitle()}

+ +
+ {#if data.joinedOrganization?.logoUrl} + Organization logo + {:else} +
+
+
+ {/if} +
+

{data.joinedOrganization?.name}

+
+
+ + {m.organizationMembership_invite_returnToDashboard()} + + {/if} +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/+error.svelte b/source/SIL.AppBuilder.Portal/src/routes/+error.svelte new file mode 100644 index 0000000000..23661e0df4 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/+error.svelte @@ -0,0 +1,5 @@ + + +

{page.status}: {page.error?.message}

diff --git a/source/SIL.AppBuilder.Portal/src/routes/+layout.server.ts b/source/SIL.AppBuilder.Portal/src/routes/+layout.server.ts new file mode 100644 index 0000000000..5bdece65b9 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/+layout.server.ts @@ -0,0 +1,6 @@ +import type { LayoutServerLoad } from './$types'; + +export const load: LayoutServerLoad = async (event) => { + const uData = await event.locals.auth(); + return { session: uData }; +}; diff --git a/source/SIL.AppBuilder.Portal/src/routes/+layout.svelte b/source/SIL.AppBuilder.Portal/src/routes/+layout.svelte new file mode 100644 index 0000000000..294b5330df --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/+layout.svelte @@ -0,0 +1,16 @@ + + + +
+ {@render children?.()} +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/+page.server.ts new file mode 100644 index 0000000000..58a9cb3021 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/+page.server.ts @@ -0,0 +1,6 @@ +import { redirect } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async (event) => { + if (await event.locals.auth()) return redirect(302, '/tasks'); +}; diff --git a/source/SIL.AppBuilder.Portal/src/routes/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/+page.svelte new file mode 100644 index 0000000000..7859c1dc47 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/+page.svelte @@ -0,0 +1,84 @@ + + +
+
+
+
+

{m.welcome()}

+

+ Scriptoria is a service that helps you keep your apps made with the App Builders software + up-to-date. +

+
+
+
+
+
+ +

{m.auth_login()}

+ + + {#snippet submitButton()} +
+ {m.auth_login()} / {m.auth_signup()} +
+ {/snippet} +
+ +
+ + {#snippet submitButton()} +
+ + {m.auth_login()} / {m.auth_signup()} with new session +
+ {/snippet} +
+
+
+ + diff --git a/source/SIL.AppBuilder.Portal/static/android-chrome-192x192.png b/source/SIL.AppBuilder.Portal/static/android-chrome-192x192.png new file mode 100644 index 0000000000..0f1d86aa0a Binary files /dev/null and b/source/SIL.AppBuilder.Portal/static/android-chrome-192x192.png differ diff --git a/source/SIL.AppBuilder.Portal/static/android-chrome-512x512.png b/source/SIL.AppBuilder.Portal/static/android-chrome-512x512.png new file mode 100644 index 0000000000..500e31b395 Binary files /dev/null and b/source/SIL.AppBuilder.Portal/static/android-chrome-512x512.png differ diff --git a/source/SIL.AppBuilder.Portal/static/apple-touch-icon.png b/source/SIL.AppBuilder.Portal/static/apple-touch-icon.png new file mode 100644 index 0000000000..45169e26af Binary files /dev/null and b/source/SIL.AppBuilder.Portal/static/apple-touch-icon.png differ diff --git a/source/SIL.AppBuilder.Portal/static/browserconfig.xml b/source/SIL.AppBuilder.Portal/static/browserconfig.xml new file mode 100644 index 0000000000..a60f6d7632 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/static/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #1c3258 + + + diff --git a/source/SIL.AppBuilder.Portal/static/favicon-16x16.png b/source/SIL.AppBuilder.Portal/static/favicon-16x16.png new file mode 100644 index 0000000000..adf11900a9 Binary files /dev/null and b/source/SIL.AppBuilder.Portal/static/favicon-16x16.png differ diff --git a/source/SIL.AppBuilder.Portal/static/favicon-32x32.png b/source/SIL.AppBuilder.Portal/static/favicon-32x32.png new file mode 100644 index 0000000000..ac51c08c92 Binary files /dev/null and b/source/SIL.AppBuilder.Portal/static/favicon-32x32.png differ diff --git a/source/SIL.AppBuilder.Portal/static/favicon.ico b/source/SIL.AppBuilder.Portal/static/favicon.ico new file mode 100644 index 0000000000..f046f57a4a Binary files /dev/null and b/source/SIL.AppBuilder.Portal/static/favicon.ico differ diff --git a/source/SIL.AppBuilder.Portal/static/mstile-150x150.png b/source/SIL.AppBuilder.Portal/static/mstile-150x150.png new file mode 100644 index 0000000000..069e2e3b6d Binary files /dev/null and b/source/SIL.AppBuilder.Portal/static/mstile-150x150.png differ diff --git a/source/SIL.AppBuilder.Portal/static/safari-pinned-tab.svg b/source/SIL.AppBuilder.Portal/static/safari-pinned-tab.svg new file mode 100644 index 0000000000..33e136d279 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/static/safari-pinned-tab.svg @@ -0,0 +1,76 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + diff --git a/source/SIL.AppBuilder.Portal/static/site.webmanifest b/source/SIL.AppBuilder.Portal/static/site.webmanifest new file mode 100644 index 0000000000..fb42a3cab0 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/static/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "Scriptoria", + "short_name": "Scriptoria", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/source/SIL.AppBuilder.Portal/svelte.config.js b/source/SIL.AppBuilder.Portal/svelte.config.js new file mode 100644 index 0000000000..8b8b4e6865 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/svelte.config.js @@ -0,0 +1,18 @@ +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + preprocess: vitePreprocess(), + + kit: { + // See https://kit.svelte.dev/docs/adapters for more information about adapters. + adapter: adapter({ + out: 'out/build' + }) + } +}; + +export default config; diff --git a/source/SIL.AppBuilder.Portal/tailwind.config.js b/source/SIL.AppBuilder.Portal/tailwind.config.js new file mode 100644 index 0000000000..afe80f2d4a --- /dev/null +++ b/source/SIL.AppBuilder.Portal/tailwind.config.js @@ -0,0 +1,96 @@ +import * as daisyui from 'daisyui'; +import * as themes from 'daisyui/src/theming/themes'; + +export default { + content: ['./src/**/*.{html,js,svelte,ts}'], + theme: { + extend: {} + }, + plugins: [daisyui], + daisyui: { + // *-focus are entirely ignored... 🤷 + themes: [ + { + scriptorialight: { + primary: '#1c3258', + 'primary-focus': '#004675', + 'primary-content': '#ffffff', + + secondary: '#575e70', + 'secondary-focus': '#7c87a2', + 'secondary-content': '#ffffff', + + accent: '#4a90e2', + 'accent-focus': '#387ed0', + 'accent-content': '#ffffff', + + neutral: '#f3f3f3', + 'neutral-focus': '#e1e1e1', + 'neutral-content': '#1e2734', + + 'base-100': '#ffffff', + 'base-200': '#f9fafb', + 'base-300': '#ced3d9', + 'base-content': '#1e2734', + + info: '#176eb5', + success: '#0cb66c', + warning: '#f9a01a', + error: '#ba1a1a', + + '--rounded-box': '1rem', + '--rounded-btn': '.5rem', + '--rounded-badge': '1.9rem', + + '--animation-btn': '.25s', + '--animation-input': '.2s', + + '--btn-text-case': 'uppercase', + '--navbar-padding': '.5rem', + '--border-btn': '1px' + }, + + scriptoriadark: { + ...themes.dark, + primary: '#1c3258', + 'primary-focus': '#002842', + 'primary-content': '#ffffff', + + secondary: '#575e70', + 'secondary-focus': '#7c87a2', + 'secondary-content': '#ffffff', + + accent: '#3560c7', + 'accent-focus': '#234eb5', + 'accent-content': '#ffffff', + + neutral: '#3b424e', + 'neutral-focus': '#2a2e37', + 'neutral-content': '#ffffff', + + 'base-content': '#ffffff', + 'base-300': '#2c3a4d', + 'base-200': '#243040', + 'base-100': '#1e2734', + + info: '#176eb5', + success: '#0cb66c', + warning: '#f9a01a', + error: '#ca2a2a', + + '--rounded-box': '1rem', + '--rounded-btn': '.5rem', + '--rounded-badge': '1.9rem', + + '--animation-btn': '.25s', + '--animation-input': '.2s', + + '--btn-text-case': 'uppercase', + '--navbar-padding': '.5rem', + '--border-btn': '1px' + } + } + ], + darkTheme: 'scriptoriadark' + } +}; diff --git a/source/SIL.AppBuilder.Portal/tests/test.ts b/source/SIL.AppBuilder.Portal/tests/test.ts new file mode 100644 index 0000000000..589fab7b00 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/tests/test.ts @@ -0,0 +1,6 @@ +import { expect, test } from '@playwright/test'; + +test('index page has expected h1', async ({ page }) => { + await page.goto('/'); + await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible(); +}); diff --git a/source/SIL.AppBuilder.Portal/tsconfig.json b/source/SIL.AppBuilder.Portal/tsconfig.json new file mode 100644 index 0000000000..794b95b642 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/source/SIL.AppBuilder.Portal/vite.config.ts b/source/SIL.AppBuilder.Portal/vite.config.ts new file mode 100644 index 0000000000..1d959d5f5e --- /dev/null +++ b/source/SIL.AppBuilder.Portal/vite.config.ts @@ -0,0 +1,112 @@ +import { paraglide } from '@inlang/paraglide-sveltekit/vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'; +import { stat, writeFile } from 'fs/promises'; +import { + createEmitAndSemanticDiagnosticsBuilderProgram, + createWatchCompilerHost, + createWatchProgram, + sys, + type BuilderProgram, + type Watch +} from 'typescript'; +import { loadEnv, searchForWorkspaceRoot } from 'vite'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + server: { + fs: { + allow: [searchForWorkspaceRoot(process.cwd()), '/common'] + } + }, + plugins: [ + { + name: 'fetch-langtags', + async buildStart() { + // Only update langtags if they are a day old + let needToRefresh = true; + try { + needToRefresh = + Date.now() - (await stat('src/lib/langtags.json')).mtimeMs > + /* One day */ 1000 * 60 * 60 * 24; + } catch { + /* empty */ + } + if (needToRefresh) { + const langtags: { + tag: string; + full: string; + name: string; + localname: string; + code: string; + regions: string[]; + }[] = await (await fetch('https://ldml.api.sil.org/langtags.json')).json(); + const parsed = langtags + .filter((tag) => !tag.tag.startsWith('_')) + .map(({ tag, full, name, localname, code, regions }) => ({ + tag, + full, + name, + localname, + code, + regions + })); + const output = JSON.stringify(parsed); + return await writeFile('src/lib/langtags.json', output); + } + } + }, + paraglide({ project: './project.inlang', outdir: './src/lib/paraglide' }), + sveltekit(), + (() => { + let executingProcess: ChildProcessWithoutNullStreams; + let running: boolean; + let watchProgram: Watch; + return { + name: 'Run BullMQ Worker', + configureServer(server) { + if (server.config.mode === 'development') { + if (watchProgram) return; + spawn('tsc', ['-w'], { + cwd: 'common', + stdio: 'pipe', + shell: true + }); + const env = Object.assign(loadEnv(server.config.mode, process.cwd()), process.env); + watchProgram = createWatchProgram( + createWatchCompilerHost( + 'node-server/tsconfig.dev.json', + {}, + sys, + createEmitAndSemanticDiagnosticsBuilderProgram, + () => {}, + async () => { + if (executingProcess) { + if (running) return; + running = true; + const prom = new Promise((r) => executingProcess.once('close', r)); + executingProcess.kill(); + await prom; + } + executingProcess = spawn('node', ['dev.js'], { + cwd: 'node-server', + stdio: 'pipe', + shell: true, + env + }); + executingProcess.stderr.on('data', (dat) => process.stderr.write(dat)); + executingProcess.stdout.on('data', (dat) => process.stdout.write(dat)); + server.httpServer?.on('close', () => executingProcess.kill()); + running = false; + } + ) + ); + } + } + }; + })() + ], + test: { + include: ['src/**/*.{test,spec}.{js,ts}'] + } +});