From 8f50795db73e80c1a16afcd57049d2bbc00225f1 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Tue, 12 Dec 2023 17:20:55 +0100 Subject: [PATCH] WIP on integrations panel --- .vscode/settings.json | 2 +- drizzle/0001_volatile_the_enforcers.sql | 134 ++ drizzle/meta/0001_snapshot.json | 1358 +++++++++++++++++ drizzle/meta/_journal.json | 7 + package.json | 6 +- public/locales/en/layout/manage.json | 3 + .../layout/Templates/ManageLayout.tsx | 13 +- .../integrations/AddIntegrationPanel.tsx | 112 +- src/pages/manage/integrations/index.tsx | 65 +- src/server/api/root.ts | 4 +- src/server/api/routers/integration.ts | 50 +- src/server/db/schema.ts | 2 +- src/types/app.ts | 5 +- yarn.lock | 10 + 14 files changed, 1616 insertions(+), 155 deletions(-) create mode 100644 drizzle/0001_volatile_the_enforcers.sql create mode 100644 drizzle/meta/0001_snapshot.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a066dae0..025acb662 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,7 +11,7 @@ "layout.manage.navigation.**", ], "editor.codeActionsOnSave": { - "source.organizeImports": true + "source.organizeImports": "explicit" }, "typescript.tsdk": "node_modules/typescript/lib", "explorer.fileNesting.patterns": { diff --git a/drizzle/0001_volatile_the_enforcers.sql b/drizzle/0001_volatile_the_enforcers.sql new file mode 100644 index 000000000..0a9600ccb --- /dev/null +++ b/drizzle/0001_volatile_the_enforcers.sql @@ -0,0 +1,134 @@ +CREATE TABLE `app_status_code` ( + `app_id` text NOT NULL, + `code` integer NOT NULL, + PRIMARY KEY(`app_id`, `code`), + FOREIGN KEY (`app_id`) REFERENCES `app`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`code`) REFERENCES `status_code`(`code`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `app` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `description` text, + `internal_url` text NOT NULL, + `external_url` text, + `icon_url` text NOT NULL, + `open_in_new_tab` integer DEFAULT false NOT NULL, + `is_ping_enabled` integer DEFAULT false NOT NULL, + `font_size` integer DEFAULT 16 NOT NULL, + `name_position` text DEFAULT 'top' NOT NULL, + `name_style` text DEFAULT 'normal' NOT NULL, + `name_line_clamp` integer DEFAULT 1 NOT NULL, + `item_id` text NOT NULL, + FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `board_integration` ( + `board_id` text NOT NULL, + `integration_id` text NOT NULL, + PRIMARY KEY(`board_id`, `integration_id`) +); +--> statement-breakpoint +CREATE TABLE `board` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `is_ping_enabled` integer DEFAULT false NOT NULL, + `allow_guests` integer DEFAULT false NOT NULL, + `page_title` text, + `meta_title` text, + `logo_image_url` text, + `favicon_image_url` text, + `background_image_url` text, + `background_image_attachment` text DEFAULT 'fixed' NOT NULL, + `background_image_repeat` text DEFAULT 'no-repeat' NOT NULL, + `background_image_size` text DEFAULT 'cover' NOT NULL, + `primary_color` text DEFAULT 'red' NOT NULL, + `secondary_color` text DEFAULT 'orange' NOT NULL, + `primary_shade` integer DEFAULT 6 NOT NULL, + `app_opacity` integer DEFAULT 100 NOT NULL, + `custom_css` text, + `user_id` text NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `integration_secret` ( + `key` text NOT NULL, + `value` text, + `integration_id` text NOT NULL, + PRIMARY KEY(`integration_id`, `key`), + FOREIGN KEY (`integration_id`) REFERENCES `integration`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `integration` ( + `id` text PRIMARY KEY NOT NULL, + `type` text NOT NULL, + `name` text NOT NULL, + `url` text NOT NULL +); +--> statement-breakpoint +CREATE TABLE `item` ( + `id` text PRIMARY KEY NOT NULL, + `kind` text NOT NULL, + `board_id` text NOT NULL, + FOREIGN KEY (`board_id`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `layout_item` ( + `id` text PRIMARY KEY NOT NULL, + `section_id` text NOT NULL, + `item_id` text NOT NULL, + `x` integer NOT NULL, + `y` integer NOT NULL, + `width` integer NOT NULL, + `height` integer NOT NULL, + FOREIGN KEY (`section_id`) REFERENCES `section`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `layout` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `kind` text NOT NULL, + `show_right_sidebar` integer DEFAULT false NOT NULL, + `show_left_sidebar` integer DEFAULT false NOT NULL, + `column_count` integer DEFAULT 10 NOT NULL, + `board_id` text NOT NULL, + FOREIGN KEY (`board_id`) REFERENCES `board`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `section` ( + `id` text PRIMARY KEY NOT NULL, + `kind` text NOT NULL, + `position` integer, + `name` text, + `layout_id` text NOT NULL, + FOREIGN KEY (`layout_id`) REFERENCES `layout`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `status_code` ( + `code` integer PRIMARY KEY NOT NULL +); +--> statement-breakpoint +CREATE TABLE `widget_integration` ( + `widget_id` text NOT NULL, + `integration_id` text NOT NULL, + PRIMARY KEY(`integration_id`, `widget_id`), + FOREIGN KEY (`widget_id`) REFERENCES `widget`(`id`) ON UPDATE no action ON DELETE cascade, + FOREIGN KEY (`integration_id`) REFERENCES `integration`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `widget_option` ( + `path` text NOT NULL, + `value` text, + `type` text NOT NULL, + `widget_id` text NOT NULL, + PRIMARY KEY(`path`, `widget_id`), + FOREIGN KEY (`widget_id`) REFERENCES `widget`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `widget` ( + `id` text PRIMARY KEY NOT NULL, + `sort` text NOT NULL, + `item_id` text NOT NULL, + FOREIGN KEY (`item_id`) REFERENCES `item`(`id`) ON UPDATE no action ON DELETE cascade +); diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 000000000..22264c08b --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,1358 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "68488752-11e7-4f48-9fd5-519d5215b076", + "prevId": "32c1bc91-e69f-4e1d-b53c-9c43f2e6c9d3", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "userId_idx": { + "name": "userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {} + }, + "app_status_code": { + "name": "app_status_code", + "columns": { + "app_id": { + "name": "app_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "code": { + "name": "code", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "app_status_code_app_id_app_id_fk": { + "name": "app_status_code_app_id_app_id_fk", + "tableFrom": "app_status_code", + "tableTo": "app", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "app_status_code_code_status_code_code_fk": { + "name": "app_status_code_code_status_code_code_fk", + "tableFrom": "app_status_code", + "tableTo": "status_code", + "columnsFrom": [ + "code" + ], + "columnsTo": [ + "code" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "app_status_code_app_id_code_pk": { + "columns": [ + "app_id", + "code" + ] + } + }, + "uniqueConstraints": {} + }, + "app": { + "name": "app", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "internal_url": { + "name": "internal_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "external_url": { + "name": "external_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "icon_url": { + "name": "icon_url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "open_in_new_tab": { + "name": "open_in_new_tab", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_ping_enabled": { + "name": "is_ping_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "font_size": { + "name": "font_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 16 + }, + "name_position": { + "name": "name_position", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'top'" + }, + "name_style": { + "name": "name_style", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'normal'" + }, + "name_line_clamp": { + "name": "name_line_clamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "app_item_id_item_id_fk": { + "name": "app_item_id_item_id_fk", + "tableFrom": "app", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "board_integration": { + "name": "board_integration", + "columns": { + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "board_integration_board_id_integration_id_pk": { + "columns": [ + "board_id", + "integration_id" + ] + } + }, + "uniqueConstraints": {} + }, + "board": { + "name": "board", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_ping_enabled": { + "name": "is_ping_enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "allow_guests": { + "name": "allow_guests", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "page_title": { + "name": "page_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta_title": { + "name": "meta_title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "logo_image_url": { + "name": "logo_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon_image_url": { + "name": "favicon_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_url": { + "name": "background_image_url", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "background_image_attachment": { + "name": "background_image_attachment", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'fixed'" + }, + "background_image_repeat": { + "name": "background_image_repeat", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'no-repeat'" + }, + "background_image_size": { + "name": "background_image_size", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'cover'" + }, + "primary_color": { + "name": "primary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'red'" + }, + "secondary_color": { + "name": "secondary_color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'orange'" + }, + "primary_shade": { + "name": "primary_shade", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 6 + }, + "app_opacity": { + "name": "app_opacity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 100 + }, + "custom_css": { + "name": "custom_css", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "board_user_id_user_id_fk": { + "name": "board_user_id_user_id_fk", + "tableFrom": "board", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "integration_secret": { + "name": "integration_secret", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "integration_secret_integration_id_integration_id_fk": { + "name": "integration_secret_integration_id_integration_id_fk", + "tableFrom": "integration_secret", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "integration_secret_integration_id_key_pk": { + "columns": [ + "integration_id", + "key" + ] + } + }, + "uniqueConstraints": {} + }, + "integration": { + "name": "integration", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "invite": { + "name": "invite", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by_id": { + "name": "created_by_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "invite_token_unique": { + "name": "invite_token_unique", + "columns": [ + "token" + ], + "isUnique": true + } + }, + "foreignKeys": { + "invite_created_by_id_user_id_fk": { + "name": "invite_created_by_id_user_id_fk", + "tableFrom": "invite", + "tableTo": "user", + "columnsFrom": [ + "created_by_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "item": { + "name": "item", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "item_board_id_board_id_fk": { + "name": "item_board_id_board_id_fk", + "tableFrom": "item", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "layout_item": { + "name": "layout_item", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "section_id": { + "name": "section_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "x": { + "name": "x", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "y": { + "name": "y", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "layout_item_section_id_section_id_fk": { + "name": "layout_item_section_id_section_id_fk", + "tableFrom": "layout_item", + "tableTo": "section", + "columnsFrom": [ + "section_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "layout_item_item_id_item_id_fk": { + "name": "layout_item_item_id_item_id_fk", + "tableFrom": "layout_item", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "layout": { + "name": "layout", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "show_right_sidebar": { + "name": "show_right_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "show_left_sidebar": { + "name": "show_left_sidebar", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "column_count": { + "name": "column_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "board_id": { + "name": "board_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "layout_board_id_board_id_fk": { + "name": "layout_board_id_board_id_fk", + "tableFrom": "layout", + "tableTo": "board", + "columnsFrom": [ + "board_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "section": { + "name": "section", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "layout_id": { + "name": "layout_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "section_layout_id_layout_id_fk": { + "name": "section_layout_id_layout_id_fk", + "tableFrom": "section", + "tableTo": "layout", + "columnsFrom": [ + "layout_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "status_code": { + "name": "status_code", + "columns": { + "code": { + "name": "code", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_setting": { + "name": "user_setting", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color_scheme": { + "name": "color_scheme", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'environment'" + }, + "language": { + "name": "language", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'en'" + }, + "default_board": { + "name": "default_board", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'default'" + }, + "first_day_of_week": { + "name": "first_day_of_week", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'monday'" + }, + "search_template": { + "name": "search_template", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'https://google.com/search?q=%s'" + }, + "open_search_in_new_tab": { + "name": "open_search_in_new_tab", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "disable_ping_pulse": { + "name": "disable_ping_pulse", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "replace_ping_with_icons": { + "name": "replace_ping_with_icons", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "use_debug_language": { + "name": "use_debug_language", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "auto_focus_search": { + "name": "auto_focus_search", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_setting_user_id_user_id_fk": { + "name": "user_setting_user_id_user_id_fk", + "tableFrom": "user_setting", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "salt": { + "name": "salt", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_admin": { + "name": "is_admin", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "is_owner": { + "name": "is_owner", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {} + }, + "widget_integration": { + "name": "widget_integration", + "columns": { + "widget_id": { + "name": "widget_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "integration_id": { + "name": "integration_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "widget_integration_widget_id_widget_id_fk": { + "name": "widget_integration_widget_id_widget_id_fk", + "tableFrom": "widget_integration", + "tableTo": "widget", + "columnsFrom": [ + "widget_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "widget_integration_integration_id_integration_id_fk": { + "name": "widget_integration_integration_id_integration_id_fk", + "tableFrom": "widget_integration", + "tableTo": "integration", + "columnsFrom": [ + "integration_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "widget_integration_widget_id_integration_id_pk": { + "columns": [ + "integration_id", + "widget_id" + ] + } + }, + "uniqueConstraints": {} + }, + "widget_option": { + "name": "widget_option", + "columns": { + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "widget_id": { + "name": "widget_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "widget_option_widget_id_widget_id_fk": { + "name": "widget_option_widget_id_widget_id_fk", + "tableFrom": "widget_option", + "tableTo": "widget", + "columnsFrom": [ + "widget_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "widget_option_widget_id_path_pk": { + "columns": [ + "path", + "widget_id" + ] + } + }, + "uniqueConstraints": {} + }, + "widget": { + "name": "widget", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "sort": { + "name": "sort", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "item_id": { + "name": "item_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "widget_item_id_item_id_fk": { + "name": "widget_item_id_item_id_fk", + "tableFrom": "widget", + "tableTo": "item", + "columnsFrom": [ + "item_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 6af8ab350..e315cd51d 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1695874816934, "tag": "0000_supreme_the_captain", "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1701527428740, + "tag": "0001_volatile_the_enforcers", + "breakpoints": true } ] } \ No newline at end of file diff --git a/package.json b/package.json index 3d81e0c49..016b5f315 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "test:coverage": "SKIP_ENV_VALIDATION=1 vitest run --coverage", "docker:build": "turbo build && docker build . -t homarr:local-dev", "docker:start": "docker run -p 7575:7575 --name homarr-development homarr:local-dev", - "db:migrate": "dotenv ts-node drizzle/migrate/migrate.ts ./drizzle" + "db:migrate": "dotenv ts-node drizzle/migrate/migrate.ts ./drizzle", + "db:create": "dotenv drizzle-kit push:sqlite" }, "dependencies": { "@auth/drizzle-adapter": "^0.3.2", @@ -92,6 +93,7 @@ "html-entities": "^2.3.3", "i18next": "^22.5.1", "immer": "^10.0.2", + "nanoid": "^5.0.4", "next": "13.4.12", "next-auth": "^4.23.0", "next-i18next": "^14.0.0", @@ -234,4 +236,4 @@ ] } } -} \ No newline at end of file +} diff --git a/public/locales/en/layout/manage.json b/public/locales/en/layout/manage.json index 013532bda..37c780ead 100644 --- a/public/locales/en/layout/manage.json +++ b/public/locales/en/layout/manage.json @@ -22,6 +22,9 @@ "contribute": "Contribute" } }, + "integrations": { + "title": "Integrations" + }, "tools": { "title": "Tools", "items": { diff --git a/src/components/layout/Templates/ManageLayout.tsx b/src/components/layout/Templates/ManageLayout.tsx index 6634597a1..95e2f37a0 100644 --- a/src/components/layout/Templates/ManageLayout.tsx +++ b/src/components/layout/Templates/ManageLayout.tsx @@ -8,10 +8,8 @@ import { Indicator, NavLink, Navbar, - Paper, Text, - ThemeIcon, - useMantineTheme, + ThemeIcon } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { @@ -21,7 +19,6 @@ import { IconBrandGithub, IconGitFork, IconHome, - IconInfoCircle, IconInfoSmall, IconLayoutDashboard, IconMailForward, @@ -29,7 +26,7 @@ import { IconTool, IconUser, IconUsers, - TablerIconsProps, + TablerIconsProps } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { useSession } from 'next-auth/react'; @@ -42,6 +39,7 @@ import { useScreenLargerThan } from '~/hooks/useScreenLargerThan'; import { usePackageAttributesStore } from '~/tools/client/zustands/usePackageAttributesStore'; import { ConditionalWrapper } from '~/utils/security'; +import { IconPlugConnected } from '@tabler/icons-react'; import { REPO_URL } from '../../../../data/constants'; import { type navigation } from '../../../../public/locales/en/layout/manage.json'; import { MainHeader } from '../header/Header'; @@ -96,6 +94,11 @@ export const ManageLayout = ({ children }: ManageLayoutProps) => { }, }, }, + integrations: { + icon: IconPlugConnected, + onlyAdmin: true, + href: '/manage/integrations', + }, tools: { icon: IconTool, onlyAdmin: true, diff --git a/src/pages/manage/integrations/AddIntegrationPanel.tsx b/src/pages/manage/integrations/AddIntegrationPanel.tsx index b23ef18a4..9e5099caf 100644 --- a/src/pages/manage/integrations/AddIntegrationPanel.tsx +++ b/src/pages/manage/integrations/AddIntegrationPanel.tsx @@ -1,98 +1,42 @@ import { Button, Group, Stack, TextInput, useMantineTheme } from '@mantine/core'; -import { UseFormReturnType, useForm } from '@mantine/form'; -import { notifications } from '@mantine/notifications'; -import { QueryKey, useQueryClient } from '@tanstack/react-query'; -import { getQueryKey } from '@trpc/react-query'; -import { getCookie } from 'cookies-next'; -import { produce } from 'immer'; +import { useForm } from '@mantine/form'; +import { useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; -import { v4 as uuidv4 } from 'uuid'; -import { IntegrationTab } from '~/components/Dashboard/Modals/EditAppModal/Tabs/IntegrationTab/IntegrationTab'; -import { AppType } from '~/types/app'; -import { IntegrationTypeMap } from '~/types/config'; -import { api } from '~/utils/api'; +import { IntegrationType } from '~/server/db/items'; +import { AppIntegrationType } from '~/types/app'; -const defaultAppValues: AppType = { - id: uuidv4(), - name: 'Your app', - url: 'https://homarr.dev', - appearance: { - iconUrl: '/imgs/logo/logo.png', - appNameStatus: 'normal', - positionAppName: '-moz-initial', - lineClampAppName: 2 - }, - network: { - enabledStatusChecker: true, - statusCodes: ['200', '301', '302', '304', '307', '308'], - okStatus: [200, 301, 302, 304, 307, 308], - }, - behaviour: { - isOpeningNewTab: true, - externalUrl: '', - }, - - area: { - type: 'wrapper', - properties: { - id: 'default', - }, - }, - shape: {}, - integration: { - id: uuidv4(), - url: '', - type: undefined, - properties: [], - name: 'New integration', - }, -}; - -export function AddIntegrationPanel({ - globalForm, - queryKey, - integrations, - setIntegrations, -}: { - globalForm: UseFormReturnType; - queryKey: QueryKey; - integrations: IntegrationTypeMap | undefined; - setIntegrations: React.Dispatch>; -}) { +export function AddIntegrationPanel({}) { const { t } = useTranslation(['board/integrations', 'common']); const queryClient = useQueryClient(); const { primaryColor } = useMantineTheme(); - const form = useForm({ - initialValues: defaultAppValues, + const form = useForm({ + initialValues: 'jellyfin', }); - if (!integrations) { - return null; - } - return (
{ - if (!integration.type || !integrations) return null; - const newIntegrations = produce(integrations, (draft) => { - integration.id = uuidv4(); - // console.log(integration.type); - if (!integration.type) return; - // If integration type is not in integrations, add it - if (!draft[integration.type]) { - draft[integration.type] = []; - } - draft[integration.type].push(integration); - }); - // queryClient.setQueryData(queryKey, newIntegrations); - form.reset(); - setIntegrations(newIntegrations); - notifications.show({ - title: t('integration.Added'), - message: t('integration.AddedDescription', { name: integration.name }), - color: 'green', - }); + onSubmit={form.onSubmit((input) => { + console.log(input); + // if (!integration.type || !integrations) return null; + // const newIntegrations = produce(integrations, (draft) => { + // integration.id = uuidv4(); + // // console.log(integration.type); + // if (!integration.type) return; + // // If integration type is not in integrations, add it + // if (!draft[integration.type]) { + // draft[integration.type] = []; + // } + // draft[integration.type].push(integration); + // }); + // // queryClient.setQueryData(queryKey, newIntegrations); + // form.reset(); + // setIntegrations(newIntegrations); + // notifications.show({ + // title: t('integration.Added'), + // message: t('integration.AddedDescription', { name: integration.name }), + // color: 'green', + // }); })} > diff --git a/src/pages/manage/integrations/index.tsx b/src/pages/manage/integrations/index.tsx index 5f877da7e..bf4327a2b 100644 --- a/src/pages/manage/integrations/index.tsx +++ b/src/pages/manage/integrations/index.tsx @@ -1,64 +1,20 @@ -import { - ActionIcon, - Autocomplete, - Avatar, - Badge, - Box, - Button, - Flex, - Group, - Pagination, - Table, - Text, - Title, - Tooltip, - useMantineTheme, -} from '@mantine/core'; +import { Text, Title } from '@mantine/core'; import { useDebouncedValue } from '@mantine/hooks'; -import { IconPlus, IconTrash, IconUserDown, IconUserUp } from '@tabler/icons-react'; import { GetServerSideProps } from 'next'; -import { useSession } from 'next-auth/react'; import { useTranslation } from 'next-i18next'; import Head from 'next/head'; -import Link from 'next/link'; import { useState } from 'react'; -import { openRoleChangeModal } from '~/components/Manage/User/change-user-role.modal'; -import { openDeleteUserModal } from '~/components/Manage/User/delete-user.modal'; import { ManageLayout } from '~/components/layout/Templates/ManageLayout'; import { getServerAuthSession } from '~/server/auth'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; import { manageNamespaces } from '~/tools/server/translation-namespaces'; import { api } from '~/utils/api'; -import { AddIntegrationPanel } from './AddIntegrationPanel'; -import { useQueryClient } from '@tanstack/react-query'; -import { IntegrationTypeMap } from '~/types/config'; -import { useConfigContext } from '~/config/provider'; -import { useForm } from '@mantine/form'; - const ManageUsersPage = () => { const [activePage, setActivePage] = useState(0); const [nonDebouncedSearch, setNonDebouncedSearch] = useState(''); const [debouncedSearch] = useDebouncedValue(nonDebouncedSearch, 200); const { t } = useTranslation('manage/integrations'); - const queryClient = useQueryClient(); - const { primaryColor } = useMantineTheme(); - const integrationsQuery: IntegrationTypeMap | undefined = queryClient.getQueryData(queryKey); - const mutation = api.config.save.useMutation(); - const { config, name } = useConfigContext(); - const [isLoading, setIsLoading] = useState(false); - const { data: sessionData } = useSession(); - const [integrations, setIntegrations] = useState( - integrationsQuery - ); - - if (!integrations) { - return null; - } - - const form = useForm({ - initialValues: integrationsQuery, - }); const metaTitle = `${t('metaTitle')} • Homarr`; @@ -70,12 +26,6 @@ const ManageUsersPage = () => { {t('pageTitle')} {t('text')} - ); }; @@ -85,15 +35,20 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { if (!session?.user.isAdmin) { return { - notFound: true, + redirect: { + destination: '/401', + permanent: false, + }, }; } + const caller = integrationsRouter + const translations = await getServerSideTranslations( - manageNamespaces, + [...manageNamespaces, 'manage/integrations'], ctx.locale, - undefined, - undefined + ctx.req, + ctx.res, ); return { props: { diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 63e2cbcc1..e8b9dd717 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -9,7 +9,7 @@ import { dnsHoleRouter } from './routers/dns-hole/router'; import { dockerRouter } from './routers/docker/router'; import { downloadRouter } from './routers/download'; import { iconRouter } from './routers/icon'; -import { integrationRouter } from './routers/integration'; +import { integrationsRouter } from './routers/integration'; import { inviteRouter } from './routers/invite/invite-router'; import { layoutsRouter } from './routers/layout/layout.router'; import { mediaRequestsRouter } from './routers/media-request'; @@ -50,7 +50,7 @@ export const rootRouter = createTRPCRouter({ password: passwordRouter, notebook: notebookRouter, layouts: layoutsRouter, - integration: integrationRouter, + integration: integrationsRouter, }); // export type definition of API diff --git a/src/server/api/routers/integration.ts b/src/server/api/routers/integration.ts index f0d7f5d84..d1e6cb3c4 100644 --- a/src/server/api/routers/integration.ts +++ b/src/server/api/routers/integration.ts @@ -1,13 +1,59 @@ import { inArray } from 'drizzle-orm'; +import { nanoid } from 'nanoid'; +import { z } from 'zod'; import { db } from '~/server/db'; -import { integrations } from '~/server/db/schema'; +import { integrationSecrets, integrations } from '~/server/db/schema'; import { adminProcedure, createTRPCRouter } from '../trpc'; -export const integrationRouter = createTRPCRouter({ +export const integrationsRouter = createTRPCRouter({ allMedia: adminProcedure.query(async () => { return await db.query.integrations.findMany({ where: inArray(integrations.type, ['jellyseerr', 'overseerr']), }); }), + + findAll: adminProcedure.query(async () => { + return await db.query.integrations.findMany({ + with: { + secrets: true, + }, + }); + }), + + addOne: adminProcedure + .input( + z.object({ + name: z.string(), + type: z.any(), + url: z.string().url(), + secrets: z.array( + z.object({ + key: z.string(), + value: z.string(), + }) + ), + }) + ) + .mutation(async ({ input }) => { + const newId = nanoid(8); + const integration = await db + .insert(integrations) + .values({ + id: newId, + name: input.name, + type: input.type, + url: input.url, + }) + .execute(); + + const secrets = await db.insert(integrationSecrets).values( + { + integrationId: newId, + key: 'apiKey', + value: 'test' + } + ); + return { integration, secrets }; + }), }); diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 330cc9f86..d0686a19c 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -1,5 +1,5 @@ import type { MantineColor } from '@mantine/core'; -import { relations, type InferSelectModel } from 'drizzle-orm'; +import { type InferSelectModel, relations } from 'drizzle-orm'; import { index, int, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core'; import { type AdapterAccount } from 'next-auth/adapters'; diff --git a/src/types/app.ts b/src/types/app.ts index 68a3c5039..f82ec2d4e 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -58,7 +58,7 @@ export type IntegrationType = | 'adGuardHome'; export type AppIntegrationType = { - type: IntegrationType | null; + type: IntegrationType; properties: AppIntegrationPropertyType[]; }; @@ -69,8 +69,7 @@ export type ConfigAppIntegrationType = Omit & export type AppIntegrationPropertyType = { type: AppIntegrationPropertyAccessabilityType; field: IntegrationField; - value?: string | null; - isDefined: boolean; + value: string; }; export type AppIntegrationPropertyAccessabilityType = 'private' | 'public'; diff --git a/yarn.lock b/yarn.lock index 893d1fa31..80c8c27d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7192,6 +7192,7 @@ __metadata: html-entities: ^2.3.3 i18next: ^22.5.1 immer: ^10.0.2 + nanoid: ^5.0.4 next: 13.4.12 next-auth: ^4.23.0 next-i18next: ^14.0.0 @@ -8929,6 +8930,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^5.0.4": + version: 5.0.4 + resolution: "nanoid@npm:5.0.4" + bin: + nanoid: bin/nanoid.js + checksum: cf09cca3774f3147100948f7478f75f4c9ee97a4af65c328dd9abbd83b12f8bb35cf9f89a21c330f3b759d667a4cd0140ed84aa5fdd522c61e0d341aeaa7fb6f + languageName: node + linkType: hard + "napi-build-utils@npm:^1.0.1": version: 1.0.2 resolution: "napi-build-utils@npm:1.0.2"