mirror of
https://github.com/apotdevin/thunderhub.git
synced 2026-08-13 12:33:08 +02:00
feat: per-channel notes (inline editing in channels table) (#744)
* feat(db): add channel_notes table with Drizzle schema and migrations Adds a new `channel_notes` table (SQLite + PG) with composite PK (account_id, channel_id) to persist per-channel notes scoped to each ThunderHub account. Includes migration files for both database backends. * feat(api): add ChannelNotesService with getChannelNotes query and setChannelNote mutation Introduces ChannelNotesService (Drizzle-backed, account-scoped) with getNotes/upsertNote methods. Exposes two new GraphQL endpoints on ChannelsResolver: getChannelNotes (query) and setChannelNote (mutation). * feat(client): add inline channel notes to channels table and details panel - Adds NoteCell component to ChannelTable with click-to-edit inline input (Enter/✓ to save, Escape to cancel) - Fetches all notes on load via getChannelNotes query; updates optimistically - Adds note textarea + Save button in ChannelDetails modal below fee editor - Notes are persisted server-side and survive page refreshes * chore: table changes * chore: schema cleanup * chore: cleanup table * chore: more cleanup * chore: db ondelete --------- Co-authored-by: Anthony Potdevin <potdevin.anthony@gmail.com>
This commit is contained in:
parent
fa1de4dd7f
commit
f62b48fa24
26 changed files with 1659 additions and 15 deletions
13
drizzle/pg/0003_lush_winter_soldier.sql
Normal file
13
drizzle/pg/0003_lush_winter_soldier.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
CREATE TABLE "channel_metadata" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"node_id" uuid NOT NULL,
|
||||
"channel_id" text NOT NULL,
|
||||
"note" text NOT NULL,
|
||||
"created_at" timestamp(6) DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp(6) DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "channel_metadata_user_id_node_id_channel_id_unique" UNIQUE("user_id","node_id","channel_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "channel_metadata" ADD CONSTRAINT "channel_metadata_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "channel_metadata" ADD CONSTRAINT "channel_metadata_node_id_nodes_id_fk" FOREIGN KEY ("node_id") REFERENCES "public"."nodes"("id") ON DELETE cascade ON UPDATE no action;
|
||||
413
drizzle/pg/meta/0003_snapshot.json
Normal file
413
drizzle/pg/meta/0003_snapshot.json
Normal file
|
|
@ -0,0 +1,413 @@
|
|||
{
|
||||
"id": "9d1326c3-8439-43b5-aa48-3b341750cb68",
|
||||
"prevId": "6e99aaac-c7ae-4c97-a3ca-12073d7da436",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.channel_metadata": {
|
||||
"name": "channel_metadata",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"node_id": {
|
||||
"name": "node_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"channel_id": {
|
||||
"name": "channel_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"note": {
|
||||
"name": "note",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"channel_metadata_user_id_users_id_fk": {
|
||||
"name": "channel_metadata_user_id_users_id_fk",
|
||||
"tableFrom": "channel_metadata",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"channel_metadata_node_id_nodes_id_fk": {
|
||||
"name": "channel_metadata_node_id_nodes_id_fk",
|
||||
"tableFrom": "channel_metadata",
|
||||
"tableTo": "nodes",
|
||||
"columnsFrom": [
|
||||
"node_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"channel_metadata_user_id_node_id_channel_id_unique": {
|
||||
"name": "channel_metadata_user_id_node_id_channel_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"user_id",
|
||||
"node_id",
|
||||
"channel_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.nodes": {
|
||||
"name": "nodes",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"team_id": {
|
||||
"name": "team_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"network": {
|
||||
"name": "network",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"socket": {
|
||||
"name": "socket",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"encrypted_macaroon": {
|
||||
"name": "encrypted_macaroon",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"encrypted_cert": {
|
||||
"name": "encrypted_cert",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"encrypted_amboss_jwt": {
|
||||
"name": "encrypted_amboss_jwt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"nodes_team_id_teams_id_fk": {
|
||||
"name": "nodes_team_id_teams_id_fk",
|
||||
"tableFrom": "nodes",
|
||||
"tableTo": "teams",
|
||||
"columnsFrom": [
|
||||
"team_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.teams": {
|
||||
"name": "teams",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user_nodes": {
|
||||
"name": "user_nodes",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"node_id": {
|
||||
"name": "node_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"user_nodes_user_id_users_id_fk": {
|
||||
"name": "user_nodes_user_id_users_id_fk",
|
||||
"tableFrom": "user_nodes",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"user_nodes_node_id_nodes_id_fk": {
|
||||
"name": "user_nodes_node_id_nodes_id_fk",
|
||||
"tableFrom": "user_nodes",
|
||||
"tableTo": "nodes",
|
||||
"columnsFrom": [
|
||||
"node_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_nodes_user_id_node_id_unique": {
|
||||
"name": "user_nodes_user_id_node_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"user_id",
|
||||
"node_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.users": {
|
||||
"name": "users",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'member'"
|
||||
},
|
||||
"team_id": {
|
||||
"name": "team_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp(6)",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"users_team_id_teams_id_fk": {
|
||||
"name": "users_team_id_teams_id_fk",
|
||||
"tableFrom": "users",
|
||||
"tableTo": "teams",
|
||||
"columnsFrom": [
|
||||
"team_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,13 @@
|
|||
"when": 1776104485702,
|
||||
"tag": "0002_add_encrypted_amboss_jwt",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "7",
|
||||
"when": 1777519591448,
|
||||
"tag": "0003_lush_winter_soldier",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
13
drizzle/sqlite/0003_orange_captain_universe.sql
Normal file
13
drizzle/sqlite/0003_orange_captain_universe.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
CREATE TABLE `channel_metadata` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`node_id` text NOT NULL,
|
||||
`channel_id` text NOT NULL,
|
||||
`note` text NOT NULL,
|
||||
`created_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||
`updated_at` text DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade,
|
||||
FOREIGN KEY (`node_id`) REFERENCES `nodes`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `channel_metadata_user_id_node_id_channel_id_unique` ON `channel_metadata` (`user_id`,`node_id`,`channel_id`);
|
||||
426
drizzle/sqlite/meta/0003_snapshot.json
Normal file
426
drizzle/sqlite/meta/0003_snapshot.json
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "62dfa64c-8696-4c03-9c25-2921fff5fd5f",
|
||||
"prevId": "2b0142c3-134e-4c4d-b5ef-e7aae8280038",
|
||||
"tables": {
|
||||
"channel_metadata": {
|
||||
"name": "channel_metadata",
|
||||
"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
|
||||
},
|
||||
"node_id": {
|
||||
"name": "node_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"channel_id": {
|
||||
"name": "channel_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"note": {
|
||||
"name": "note",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"channel_metadata_user_id_node_id_channel_id_unique": {
|
||||
"name": "channel_metadata_user_id_node_id_channel_id_unique",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"node_id",
|
||||
"channel_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"channel_metadata_user_id_users_id_fk": {
|
||||
"name": "channel_metadata_user_id_users_id_fk",
|
||||
"tableFrom": "channel_metadata",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"channel_metadata_node_id_nodes_id_fk": {
|
||||
"name": "channel_metadata_node_id_nodes_id_fk",
|
||||
"tableFrom": "channel_metadata",
|
||||
"tableTo": "nodes",
|
||||
"columnsFrom": [
|
||||
"node_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"nodes": {
|
||||
"name": "nodes",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"team_id": {
|
||||
"name": "team_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"network": {
|
||||
"name": "network",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"socket": {
|
||||
"name": "socket",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"encrypted_macaroon": {
|
||||
"name": "encrypted_macaroon",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"encrypted_cert": {
|
||||
"name": "encrypted_cert",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"encrypted_amboss_jwt": {
|
||||
"name": "encrypted_amboss_jwt",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"nodes_team_id_teams_id_fk": {
|
||||
"name": "nodes_team_id_teams_id_fk",
|
||||
"tableFrom": "nodes",
|
||||
"tableTo": "teams",
|
||||
"columnsFrom": [
|
||||
"team_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"teams": {
|
||||
"name": "teams",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_nodes": {
|
||||
"name": "user_nodes",
|
||||
"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
|
||||
},
|
||||
"node_id": {
|
||||
"name": "node_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_nodes_user_id_node_id_unique": {
|
||||
"name": "user_nodes_user_id_node_id_unique",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"node_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_nodes_user_id_users_id_fk": {
|
||||
"name": "user_nodes_user_id_users_id_fk",
|
||||
"tableFrom": "user_nodes",
|
||||
"tableTo": "users",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"user_nodes_node_id_nodes_id_fk": {
|
||||
"name": "user_nodes_node_id_nodes_id_fk",
|
||||
"tableFrom": "user_nodes",
|
||||
"tableTo": "nodes",
|
||||
"columnsFrom": [
|
||||
"node_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'member'"
|
||||
},
|
||||
"team_id": {
|
||||
"name": "team_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"users_team_id_teams_id_fk": {
|
||||
"name": "users_team_id_teams_id_fk",
|
||||
"tableFrom": "users",
|
||||
"tableTo": "teams",
|
||||
"columnsFrom": [
|
||||
"team_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,13 @@
|
|||
"when": 1776104481313,
|
||||
"tag": "0002_add_encrypted_amboss_jwt",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "6",
|
||||
"when": 1777519591063,
|
||||
"tag": "0003_orange_captain_universe",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
21
schema.gql
21
schema.gql
|
|
@ -160,6 +160,7 @@ type Channel {
|
|||
is_private: Boolean!
|
||||
local_balance: Float!
|
||||
local_reserve: Float!
|
||||
note: String
|
||||
partner_fee_info: SingleChannel
|
||||
partner_node_info: Node!
|
||||
partner_public_key: String!
|
||||
|
|
@ -193,6 +194,12 @@ type ChannelInfo {
|
|||
node2_info: BaseNodeInfo!
|
||||
}
|
||||
|
||||
type ChannelMetadata {
|
||||
channel_id: String!
|
||||
note: String!
|
||||
updated_at: String!
|
||||
}
|
||||
|
||||
type ChannelReport {
|
||||
commit: Float!
|
||||
incomingPendingHtlc: Float!
|
||||
|
|
@ -219,6 +226,11 @@ type ChannelSummary {
|
|||
total_remote_sats: String!
|
||||
}
|
||||
|
||||
type ChannelsMutations {
|
||||
delete_note(channelId: String!): Boolean!
|
||||
upsert_note(channelId: String!, note: String!): ChannelMetadata!
|
||||
}
|
||||
|
||||
type ClosedChannel {
|
||||
capacity: Float!
|
||||
channel_age: Float
|
||||
|
|
@ -569,6 +581,7 @@ type Mutation {
|
|||
updateFees(base_fee_tokens: Float, cltv_delta: Float, fee_rate: Float, max_htlc_mtokens: String, min_htlc_mtokens: String, transaction_id: String, transaction_vout: Float): Boolean!
|
||||
updateMultipleFees(channels: [UpdateRoutingFeesParams!]!): Boolean!
|
||||
updateTwofaSecret(secret: String!, token: String!): Boolean!
|
||||
user: UserMutations!
|
||||
}
|
||||
|
||||
type NetworkInfo {
|
||||
|
|
@ -660,6 +673,10 @@ type NodeType {
|
|||
public_key: String!
|
||||
}
|
||||
|
||||
type OffchainMutations {
|
||||
channels: ChannelsMutations!
|
||||
}
|
||||
|
||||
input OfferReadinessInput {
|
||||
peer_pubkey: String!
|
||||
tapd_asset_id: String
|
||||
|
|
@ -1340,6 +1357,10 @@ type UserBackupInfo {
|
|||
total_size_saved: String!
|
||||
}
|
||||
|
||||
type UserMutations {
|
||||
offchain: OffchainMutations!
|
||||
}
|
||||
|
||||
type UserNode {
|
||||
id: String!
|
||||
name: String!
|
||||
|
|
|
|||
157
src/client/src/graphql/mutations/__generated__/setChannelNote.generated.tsx
generated
Normal file
157
src/client/src/graphql/mutations/__generated__/setChannelNote.generated.tsx
generated
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import * as Types from '../../types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type UpsertChannelNoteMutationVariables = Types.Exact<{
|
||||
channelId: Types.Scalars['String']['input'];
|
||||
note: Types.Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
export type UpsertChannelNoteMutation = {
|
||||
__typename?: 'Mutation';
|
||||
user: {
|
||||
__typename?: 'UserMutations';
|
||||
offchain: {
|
||||
__typename?: 'OffchainMutations';
|
||||
channels: {
|
||||
__typename?: 'ChannelsMutations';
|
||||
upsert_note: {
|
||||
__typename?: 'ChannelMetadata';
|
||||
channel_id: string;
|
||||
note: string;
|
||||
updated_at: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type DeleteChannelNoteMutationVariables = Types.Exact<{
|
||||
channelId: Types.Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
export type DeleteChannelNoteMutation = {
|
||||
__typename?: 'Mutation';
|
||||
user: {
|
||||
__typename?: 'UserMutations';
|
||||
offchain: {
|
||||
__typename?: 'OffchainMutations';
|
||||
channels: { __typename?: 'ChannelsMutations'; delete_note: boolean };
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const UpsertChannelNoteDocument = gql`
|
||||
mutation UpsertChannelNote($channelId: String!, $note: String!) {
|
||||
user {
|
||||
offchain {
|
||||
channels {
|
||||
upsert_note(channelId: $channelId, note: $note) {
|
||||
channel_id
|
||||
note
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type UpsertChannelNoteMutationFn = Apollo.MutationFunction<
|
||||
UpsertChannelNoteMutation,
|
||||
UpsertChannelNoteMutationVariables
|
||||
>;
|
||||
|
||||
/**
|
||||
* __useUpsertChannelNoteMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useUpsertChannelNoteMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useUpsertChannelNoteMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [upsertChannelNoteMutation, { data, loading, error }] = useUpsertChannelNoteMutation({
|
||||
* variables: {
|
||||
* channelId: // value for 'channelId'
|
||||
* note: // value for 'note'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useUpsertChannelNoteMutation(
|
||||
baseOptions?: Apollo.MutationHookOptions<
|
||||
UpsertChannelNoteMutation,
|
||||
UpsertChannelNoteMutationVariables
|
||||
>
|
||||
) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useMutation<
|
||||
UpsertChannelNoteMutation,
|
||||
UpsertChannelNoteMutationVariables
|
||||
>(UpsertChannelNoteDocument, options);
|
||||
}
|
||||
export type UpsertChannelNoteMutationHookResult = ReturnType<
|
||||
typeof useUpsertChannelNoteMutation
|
||||
>;
|
||||
export type UpsertChannelNoteMutationResult =
|
||||
Apollo.MutationResult<UpsertChannelNoteMutation>;
|
||||
export type UpsertChannelNoteMutationOptions = Apollo.BaseMutationOptions<
|
||||
UpsertChannelNoteMutation,
|
||||
UpsertChannelNoteMutationVariables
|
||||
>;
|
||||
export const DeleteChannelNoteDocument = gql`
|
||||
mutation DeleteChannelNote($channelId: String!) {
|
||||
user {
|
||||
offchain {
|
||||
channels {
|
||||
delete_note(channelId: $channelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type DeleteChannelNoteMutationFn = Apollo.MutationFunction<
|
||||
DeleteChannelNoteMutation,
|
||||
DeleteChannelNoteMutationVariables
|
||||
>;
|
||||
|
||||
/**
|
||||
* __useDeleteChannelNoteMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useDeleteChannelNoteMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useDeleteChannelNoteMutation` returns a tuple that includes:
|
||||
* - A mutate function that you can call at any time to execute the mutation
|
||||
* - An object with fields that represent the current status of the mutation's execution
|
||||
*
|
||||
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
|
||||
*
|
||||
* @example
|
||||
* const [deleteChannelNoteMutation, { data, loading, error }] = useDeleteChannelNoteMutation({
|
||||
* variables: {
|
||||
* channelId: // value for 'channelId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDeleteChannelNoteMutation(
|
||||
baseOptions?: Apollo.MutationHookOptions<
|
||||
DeleteChannelNoteMutation,
|
||||
DeleteChannelNoteMutationVariables
|
||||
>
|
||||
) {
|
||||
const options = { ...defaultOptions, ...baseOptions };
|
||||
return Apollo.useMutation<
|
||||
DeleteChannelNoteMutation,
|
||||
DeleteChannelNoteMutationVariables
|
||||
>(DeleteChannelNoteDocument, options);
|
||||
}
|
||||
export type DeleteChannelNoteMutationHookResult = ReturnType<
|
||||
typeof useDeleteChannelNoteMutation
|
||||
>;
|
||||
export type DeleteChannelNoteMutationResult =
|
||||
Apollo.MutationResult<DeleteChannelNoteMutation>;
|
||||
export type DeleteChannelNoteMutationOptions = Apollo.BaseMutationOptions<
|
||||
DeleteChannelNoteMutation,
|
||||
DeleteChannelNoteMutationVariables
|
||||
>;
|
||||
29
src/client/src/graphql/mutations/setChannelNote.ts
Normal file
29
src/client/src/graphql/mutations/setChannelNote.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPSERT_CHANNEL_NOTE = gql`
|
||||
mutation UpsertChannelNote($channelId: String!, $note: String!) {
|
||||
user {
|
||||
offchain {
|
||||
channels {
|
||||
upsert_note(channelId: $channelId, note: $note) {
|
||||
channel_id
|
||||
note
|
||||
updated_at
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_CHANNEL_NOTE = gql`
|
||||
mutation DeleteChannelNote($channelId: String!) {
|
||||
user {
|
||||
offchain {
|
||||
channels {
|
||||
delete_note(channelId: $channelId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -34,6 +34,7 @@ export type GetChannelsQuery = {
|
|||
transaction_vout: number;
|
||||
unsettled_balance: number;
|
||||
channel_age: number;
|
||||
note?: string | null;
|
||||
pending_resume: {
|
||||
__typename?: 'PendingResume';
|
||||
incoming_tokens: number;
|
||||
|
|
@ -123,6 +124,7 @@ export const GetChannelsDocument = gql`
|
|||
transaction_vout
|
||||
unsettled_balance
|
||||
channel_age
|
||||
note
|
||||
pending_resume {
|
||||
incoming_tokens
|
||||
outgoing_tokens
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export const GET_CHANNELS = gql`
|
|||
transaction_vout
|
||||
unsettled_balance
|
||||
channel_age
|
||||
note
|
||||
pending_resume {
|
||||
incoming_tokens
|
||||
outgoing_tokens
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@ export type Channel = {
|
|||
is_private: Scalars['Boolean']['output'];
|
||||
local_balance: Scalars['Float']['output'];
|
||||
local_reserve: Scalars['Float']['output'];
|
||||
note?: Maybe<Scalars['String']['output']>;
|
||||
partner_fee_info?: Maybe<SingleChannel>;
|
||||
partner_node_info: Node;
|
||||
partner_public_key: Scalars['String']['output'];
|
||||
|
|
@ -237,6 +238,13 @@ export type ChannelInfo = {
|
|||
node2_info: BaseNodeInfo;
|
||||
};
|
||||
|
||||
export type ChannelMetadata = {
|
||||
__typename?: 'ChannelMetadata';
|
||||
channel_id: Scalars['String']['output'];
|
||||
note: Scalars['String']['output'];
|
||||
updated_at: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type ChannelReport = {
|
||||
__typename?: 'ChannelReport';
|
||||
commit: Scalars['Float']['output'];
|
||||
|
|
@ -266,6 +274,21 @@ export type ChannelSummary = {
|
|||
total_remote_sats: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type ChannelsMutations = {
|
||||
__typename?: 'ChannelsMutations';
|
||||
delete_note: Scalars['Boolean']['output'];
|
||||
upsert_note: ChannelMetadata;
|
||||
};
|
||||
|
||||
export type ChannelsMutationsDelete_NoteArgs = {
|
||||
channelId: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type ChannelsMutationsUpsert_NoteArgs = {
|
||||
channelId: Scalars['String']['input'];
|
||||
note: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
export type ClosedChannel = {
|
||||
__typename?: 'ClosedChannel';
|
||||
capacity: Scalars['Float']['output'];
|
||||
|
|
@ -662,6 +685,7 @@ export type Mutation = {
|
|||
updateFees: Scalars['Boolean']['output'];
|
||||
updateMultipleFees: Scalars['Boolean']['output'];
|
||||
updateTwofaSecret: Scalars['Boolean']['output'];
|
||||
user: UserMutations;
|
||||
};
|
||||
|
||||
export type MutationAddPeerArgs = {
|
||||
|
|
@ -904,6 +928,11 @@ export type NodeType = {
|
|||
public_key: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type OffchainMutations = {
|
||||
__typename?: 'OffchainMutations';
|
||||
channels: ChannelsMutations;
|
||||
};
|
||||
|
||||
export type OfferReadinessInput = {
|
||||
peer_pubkey: Scalars['String']['input'];
|
||||
tapd_asset_id?: InputMaybe<Scalars['String']['input']>;
|
||||
|
|
@ -916,8 +945,8 @@ export type OfferReadinessResult = {
|
|||
btc_channels: ChannelSummary;
|
||||
has_pending_order: Scalars['Boolean']['output'];
|
||||
is_peer_connected: Scalars['Boolean']['output'];
|
||||
onchain_balance_sats: Scalars['String']['output'];
|
||||
onchain_asset_balance: Scalars['String']['output'];
|
||||
onchain_balance_sats: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type OnChainBalance = {
|
||||
|
|
@ -1807,6 +1836,11 @@ export type UserBackupInfo = {
|
|||
total_size_saved: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type UserMutations = {
|
||||
__typename?: 'UserMutations';
|
||||
offchain: OffchainMutations;
|
||||
};
|
||||
|
||||
export type UserNode = {
|
||||
__typename?: 'UserNode';
|
||||
id: Scalars['String']['output'];
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ import { Loader2 } from 'lucide-react';
|
|||
import { ChangeDetails } from '../../../components/modal/changeDetails/ChangeDetails';
|
||||
import { useGetChannelInfoQuery } from '../../../graphql/queries/__generated__/getChannel.generated';
|
||||
|
||||
export const ChannelDetails: FC<{ id?: string; name?: string }> = ({
|
||||
id = '',
|
||||
name = '',
|
||||
}) => {
|
||||
export const ChannelDetails: FC<{
|
||||
id?: string;
|
||||
name?: string;
|
||||
}> = ({ id = '', name = '' }) => {
|
||||
const { data, loading, error } = useGetChannelInfoQuery({
|
||||
variables: { id },
|
||||
skip: !id,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,28 @@
|
|||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { ArrowDown, ArrowUp, Check, Circle, Edit, X } from 'lucide-react';
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Check,
|
||||
Circle,
|
||||
Edit,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip';
|
||||
import { BalanceBars } from '../../../components/balance';
|
||||
import {
|
||||
getChannelLink,
|
||||
|
|
@ -12,6 +34,10 @@ import Modal from '../../../components/modal/ReactModal';
|
|||
import { Price } from '../../../components/price/Price';
|
||||
import Table from '../../../components/table';
|
||||
import { useGetChannelsQuery } from '../../../graphql/queries/__generated__/getChannels.generated';
|
||||
import {
|
||||
useUpsertChannelNoteMutation,
|
||||
useDeleteChannelNoteMutation,
|
||||
} from '../../../graphql/mutations/__generated__/setChannelNote.generated';
|
||||
import { useLocalStorage } from '../../../hooks/UseLocalStorage';
|
||||
import { useChartColors } from '../../../lib/chart-colors';
|
||||
import { getErrorContent } from '../../../utils/error';
|
||||
|
|
@ -22,6 +48,7 @@ import {
|
|||
getPercent,
|
||||
} from '../../../utils/helpers';
|
||||
import { colorFromString } from '../../../utils/color';
|
||||
import { useAccount } from '../../../hooks/UseAccount';
|
||||
import { ChannelDetails } from './ChannelDetails';
|
||||
import { defaultHiddenColumns } from './helpers';
|
||||
import { VisibilityState } from '@tanstack/react-table';
|
||||
|
|
@ -33,11 +60,155 @@ const getBar = (top: number, bottom: number) => {
|
|||
|
||||
const REMOTE_COLOR = 'rgba(209, 213, 219, 0.6)';
|
||||
|
||||
// ── NoteCell ──────────────────────────────────────────────────────────────────
|
||||
|
||||
type NoteCellProps = {
|
||||
note: string;
|
||||
channelId: string;
|
||||
isDbAccount: boolean;
|
||||
};
|
||||
|
||||
const NoteCell = ({ note, channelId, isDbAccount }: NoteCellProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [value, setValue] = useState(note);
|
||||
const [upsertNote, { loading: saving }] = useUpsertChannelNoteMutation();
|
||||
const [deleteNote, { loading: deleting }] = useDeleteChannelNoteMutation();
|
||||
|
||||
const updateCache = (newNote: string) => (cache: any) => {
|
||||
cache.modify({
|
||||
id: cache.identify({ __typename: 'Channel', id: channelId }),
|
||||
fields: { note: () => newNote },
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setValue(note);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
await upsertNote({
|
||||
variables: { channelId, note: value },
|
||||
update: updateCache(value),
|
||||
});
|
||||
setOpen(false);
|
||||
} catch {
|
||||
toast.error('Failed to save note');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
try {
|
||||
await deleteNote({
|
||||
variables: { channelId },
|
||||
update: updateCache(''),
|
||||
});
|
||||
setOpen(false);
|
||||
} catch {
|
||||
toast.error('Failed to delete note');
|
||||
}
|
||||
};
|
||||
|
||||
const truncated = note.length > 32 ? `${note.slice(0, 32)}...` : note;
|
||||
|
||||
const trigger = (
|
||||
<button
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleOpen();
|
||||
}}
|
||||
className="inline-flex items-center gap-1.5 border-none bg-transparent p-0 cursor-pointer text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{note ? (
|
||||
<>
|
||||
<span className="text-xs max-w-[120px] truncate">{truncated}</span>
|
||||
<Edit size={14} className="shrink-0" />
|
||||
</>
|
||||
) : (
|
||||
<span className="text-base opacity-30 select-none">+</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{note.length > 10 ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{trigger}</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs wrap-break-words">
|
||||
{note}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
trigger
|
||||
)}
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent onClick={e => e.stopPropagation()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Channel Note</DialogTitle>
|
||||
</DialogHeader>
|
||||
{isDbAccount ? (
|
||||
<>
|
||||
<Input
|
||||
autoFocus
|
||||
value={value}
|
||||
maxLength={500}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
placeholder="Personal note for this channel..."
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter' && value.trim()) handleSave();
|
||||
}}
|
||||
/>
|
||||
<DialogFooter>
|
||||
{note && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={deleting}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={saving || !value.trim()}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground text-balance">
|
||||
Channel notes require a database account.{' '}
|
||||
<a
|
||||
href="https://docs.thunderhub.io/setup#database-optional"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline underline-offset-3 hover:text-foreground"
|
||||
>
|
||||
Learn how to set up a database.
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// ── ChannelTable ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const ChannelTable = ({
|
||||
assetOnly,
|
||||
storageKey = 'hiddenColumns-v2',
|
||||
}: { assetOnly?: boolean; storageKey?: string } = {}) => {
|
||||
const chartColors = useChartColors();
|
||||
const account = useAccount();
|
||||
const isDbAccount = account?.type === 'db';
|
||||
|
||||
const [channel, setChannel] = useState<{
|
||||
name: string;
|
||||
|
|
@ -200,7 +371,6 @@ export const ChannelTable = ({
|
|||
),
|
||||
};
|
||||
|
||||
// Build dynamic asset fields for each unique asset
|
||||
const assetFields: Record<string, any> = {};
|
||||
for (const [key, assetInfo] of uniqueAssets) {
|
||||
const channelAssetKey = c.asset
|
||||
|
|
@ -258,6 +428,7 @@ export const ChannelTable = ({
|
|||
...partnerInfo,
|
||||
...actions,
|
||||
...assetFields,
|
||||
note: c.note ?? '',
|
||||
alias: c.partner_node_info.node?.alias || 'Unknown',
|
||||
undercaseAlias: (
|
||||
c.partner_node_info.node?.alias || 'Unknown'
|
||||
|
|
@ -403,6 +574,18 @@ export const ChannelTable = ({
|
|||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Note',
|
||||
accessorKey: 'note',
|
||||
enableSorting: false,
|
||||
cell: ({ row }: any) => (
|
||||
<NoteCell
|
||||
note={row.original.note}
|
||||
channelId={row.original.id}
|
||||
isDbAccount={isDbAccount}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: 'Capacity',
|
||||
accessorKey: 'capacity',
|
||||
|
|
@ -641,7 +824,7 @@ export const ChannelTable = ({
|
|||
};
|
||||
}),
|
||||
],
|
||||
[numberStringSorting, uniqueAssets]
|
||||
[numberStringSorting, uniqueAssets, isDbAccount]
|
||||
);
|
||||
|
||||
const handleToggle = (hide: boolean, id: string) => {
|
||||
|
|
|
|||
90
src/server/modules/api/channels/channel-metadata.service.ts
Normal file
90
src/server/modules/api/channels/channel-metadata.service.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { DRIZZLE, DrizzleProvider } from '../../database/drizzle.provider';
|
||||
import { ChannelMetadata } from './channel-metadata.types';
|
||||
|
||||
@Injectable()
|
||||
export class ChannelMetadataService {
|
||||
constructor(@Inject(DRIZZLE) private readonly drizzle: DrizzleProvider) {}
|
||||
|
||||
async getNotesByNode(
|
||||
userId: string,
|
||||
nodeId: string
|
||||
): Promise<Map<string, string>> {
|
||||
if (!this.drizzle) return new Map();
|
||||
const { db, schema } = this.drizzle;
|
||||
const rows = await (db as any)
|
||||
.select({
|
||||
channel_id: schema.channelMetadata.channel_id,
|
||||
note: schema.channelMetadata.note,
|
||||
})
|
||||
.from(schema.channelMetadata)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.channelMetadata.user_id, userId),
|
||||
eq(schema.channelMetadata.node_id, nodeId)
|
||||
)
|
||||
);
|
||||
const map = new Map<string, string>();
|
||||
for (const row of rows) {
|
||||
map.set(row.channel_id, row.note);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
async upsertNote(
|
||||
userId: string,
|
||||
nodeId: string,
|
||||
channelId: string,
|
||||
note: string
|
||||
): Promise<ChannelMetadata> {
|
||||
if (!this.drizzle) {
|
||||
throw new Error(
|
||||
'Channel notes require a database. Set DB_TYPE and DB_SQLITE_PATH (or DB_POSTGRES_URL) in your environment.'
|
||||
);
|
||||
}
|
||||
const { db, schema } = this.drizzle;
|
||||
const now = new Date().toISOString();
|
||||
await (db as any)
|
||||
.insert(schema.channelMetadata)
|
||||
.values({
|
||||
user_id: userId,
|
||||
node_id: nodeId,
|
||||
channel_id: channelId,
|
||||
note,
|
||||
updated_at: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
schema.channelMetadata.user_id,
|
||||
schema.channelMetadata.node_id,
|
||||
schema.channelMetadata.channel_id,
|
||||
],
|
||||
set: { note, updated_at: now },
|
||||
});
|
||||
return { channel_id: channelId, note, updated_at: now };
|
||||
}
|
||||
|
||||
async deleteNote(
|
||||
userId: string,
|
||||
nodeId: string,
|
||||
channelId: string
|
||||
): Promise<boolean> {
|
||||
if (!this.drizzle) {
|
||||
throw new Error(
|
||||
'Channel notes require a database. Set DB_TYPE and DB_SQLITE_PATH (or DB_POSTGRES_URL) in your environment.'
|
||||
);
|
||||
}
|
||||
const { db, schema } = this.drizzle;
|
||||
await (db as any)
|
||||
.delete(schema.channelMetadata)
|
||||
.where(
|
||||
and(
|
||||
eq(schema.channelMetadata.user_id, userId),
|
||||
eq(schema.channelMetadata.node_id, nodeId),
|
||||
eq(schema.channelMetadata.channel_id, channelId)
|
||||
)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
34
src/server/modules/api/channels/channel-metadata.types.ts
Normal file
34
src/server/modules/api/channels/channel-metadata.types.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class ChannelMetadata {
|
||||
@Field(() => String)
|
||||
channel_id: string;
|
||||
|
||||
@Field(() => String)
|
||||
note: string;
|
||||
|
||||
@Field(() => String)
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class ChannelsMutations {
|
||||
@Field(() => ChannelMetadata)
|
||||
upsert_note: ChannelMetadata;
|
||||
|
||||
@Field(() => Boolean)
|
||||
delete_note: boolean;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class OffchainMutations {
|
||||
@Field(() => ChannelsMutations)
|
||||
channels: ChannelsMutations;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class UserMutations {
|
||||
@Field(() => OffchainMutations)
|
||||
offchain: OffchainMutations;
|
||||
}
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import { Inject } from '@nestjs/common';
|
||||
import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import { Context, Parent, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { toWithError } from 'src/server/utils/async';
|
||||
import { Logger } from 'winston';
|
||||
import { ContextType } from 'src/server/app.module';
|
||||
import { NodeService } from '../../node/node.service';
|
||||
import { CurrentUser } from '../../security/security.decorators';
|
||||
import { UserId } from '../../security/security.types';
|
||||
import { AuthType, UserId } from '../../security/security.types';
|
||||
import { Channel, SingleChannelParentType } from './channels.types';
|
||||
|
||||
@Resolver(Channel)
|
||||
|
|
@ -101,4 +102,20 @@ export class ChannelResolver {
|
|||
partner_node_policies,
|
||||
};
|
||||
}
|
||||
|
||||
@ResolveField(() => String, { nullable: true })
|
||||
async note(
|
||||
@CurrentUser() user: UserId,
|
||||
@Parent() { id }: Channel,
|
||||
@Context() { loaders }: ContextType
|
||||
): Promise<string | null> {
|
||||
if (user.authType !== AuthType.USER) return null;
|
||||
const dbUserId = user.userId ?? user.id;
|
||||
const nodeId = user.id;
|
||||
return loaders.channelNotesLoader.load({
|
||||
userId: dbUserId,
|
||||
nodeId,
|
||||
channelId: id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,28 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { NodeModule } from '../../node/node.module';
|
||||
import { ChannelResolver } from './channel.resolver';
|
||||
import { ChannelsResolver } from './channels.resolver';
|
||||
import {
|
||||
ChannelsResolver,
|
||||
UserMutationRoot,
|
||||
UserMutationsResolver,
|
||||
OffchainMutationsResolver,
|
||||
ChannelsMutationsResolver,
|
||||
} from './channels.resolver';
|
||||
import { ChannelMetadataService } from './channel-metadata.service';
|
||||
import { FetchModule } from '../../fetch/fetch.module';
|
||||
import { AmbossModule } from '../amboss/amboss.module';
|
||||
import { TapdModule } from '../../node/tapd/tapd.module';
|
||||
|
||||
@Module({
|
||||
imports: [NodeModule, FetchModule, AmbossModule, TapdModule],
|
||||
providers: [ChannelsResolver, ChannelResolver],
|
||||
providers: [
|
||||
ChannelsResolver,
|
||||
ChannelResolver,
|
||||
ChannelMetadataService,
|
||||
UserMutationRoot,
|
||||
UserMutationsResolver,
|
||||
OffchainMutationsResolver,
|
||||
ChannelsMutationsResolver,
|
||||
],
|
||||
})
|
||||
export class ChannelsModule {}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Inject } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { Args, Mutation, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||
import { toWithError } from 'src/server/utils/async';
|
||||
import { Logger } from 'winston';
|
||||
|
|
@ -23,6 +23,13 @@ import { FetchService } from '../../fetch/fetch.service';
|
|||
import { GetRecommendedNode } from '../amboss/amboss.gql';
|
||||
import { AmbossService } from '../amboss/amboss.service';
|
||||
import { TapdNodeService } from '../../node/tapd/tapd-node.service';
|
||||
import {
|
||||
ChannelMetadata,
|
||||
ChannelsMutations,
|
||||
OffchainMutations,
|
||||
UserMutations,
|
||||
} from './channel-metadata.types';
|
||||
import { ChannelMetadataService } from './channel-metadata.service';
|
||||
|
||||
function toAssetField(ac: {
|
||||
assetId: string;
|
||||
|
|
@ -453,3 +460,70 @@ export class ChannelsResolver {
|
|||
return errors ? false : true;
|
||||
}
|
||||
}
|
||||
|
||||
@Resolver()
|
||||
export class UserMutationRoot {
|
||||
@Mutation(() => UserMutations)
|
||||
async user(): Promise<UserMutations> {
|
||||
return {} as any;
|
||||
}
|
||||
}
|
||||
|
||||
@Resolver(() => UserMutations)
|
||||
export class UserMutationsResolver {
|
||||
@ResolveField(() => OffchainMutations)
|
||||
async offchain(): Promise<OffchainMutations> {
|
||||
return {} as any;
|
||||
}
|
||||
}
|
||||
|
||||
@Resolver(() => OffchainMutations)
|
||||
export class OffchainMutationsResolver {
|
||||
@ResolveField(() => ChannelsMutations)
|
||||
async channels(): Promise<ChannelsMutations> {
|
||||
return {} as any;
|
||||
}
|
||||
}
|
||||
|
||||
@Resolver(() => ChannelsMutations)
|
||||
export class ChannelsMutationsResolver {
|
||||
constructor(private channelMetadataService: ChannelMetadataService) {}
|
||||
|
||||
@ResolveField(() => ChannelMetadata)
|
||||
async upsert_note(
|
||||
@CurrentUser() user: UserId,
|
||||
@Args('channelId') channelId: string,
|
||||
@Args('note') note: string
|
||||
): Promise<ChannelMetadata> {
|
||||
if (!/^\d+x\d+x\d+$/.test(channelId)) {
|
||||
throw new GraphQLError('Invalid channel ID format.');
|
||||
}
|
||||
if (!note.trim()) {
|
||||
throw new GraphQLError('Note cannot be empty.');
|
||||
}
|
||||
if (note.length > 500) {
|
||||
throw new GraphQLError('Note must be 500 characters or fewer.');
|
||||
}
|
||||
const dbUserId = user.userId ?? user.id;
|
||||
const nodeId = user.id;
|
||||
return this.channelMetadataService.upsertNote(
|
||||
dbUserId,
|
||||
nodeId,
|
||||
channelId,
|
||||
note
|
||||
);
|
||||
}
|
||||
|
||||
@ResolveField(() => Boolean)
|
||||
async delete_note(
|
||||
@CurrentUser() user: UserId,
|
||||
@Args('channelId') channelId: string
|
||||
): Promise<boolean> {
|
||||
if (!/^\d+x\d+x\d+$/.test(channelId)) {
|
||||
throw new GraphQLError('Invalid channel ID format.');
|
||||
}
|
||||
const dbUserId = user.userId ?? user.id;
|
||||
const nodeId = user.id;
|
||||
return this.channelMetadataService.deleteNote(dbUserId, nodeId, channelId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,6 +169,8 @@ export class Channel {
|
|||
pending_resume: PendingResume;
|
||||
@Field(() => ChannelAsset, { nullable: true })
|
||||
asset?: ChannelAsset;
|
||||
@Field({ nullable: true })
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export type SingleChannelParentType = {
|
||||
|
|
|
|||
28
src/server/modules/database/schema/pg/channel-metadata.ts
Normal file
28
src/server/modules/database/schema/pg/channel-metadata.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { pgTable, uuid, text, timestamp, unique } from 'drizzle-orm/pg-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { users } from './users';
|
||||
import { nodes } from './nodes';
|
||||
|
||||
export const channelMetadata = pgTable(
|
||||
'channel_metadata',
|
||||
{
|
||||
id: uuid('id')
|
||||
.primaryKey()
|
||||
.default(sql`gen_random_uuid()`),
|
||||
user_id: uuid('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
node_id: uuid('node_id')
|
||||
.notNull()
|
||||
.references(() => nodes.id, { onDelete: 'cascade' }),
|
||||
channel_id: text('channel_id').notNull(),
|
||||
note: text('note').notNull(),
|
||||
created_at: timestamp('created_at', { precision: 6, mode: 'string' })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
updated_at: timestamp('updated_at', { precision: 6, mode: 'string' })
|
||||
.notNull()
|
||||
.defaultNow(),
|
||||
},
|
||||
t => [unique().on(t.user_id, t.node_id, t.channel_id)]
|
||||
);
|
||||
|
|
@ -2,3 +2,4 @@ export { teams } from './teams';
|
|||
export { users } from './users';
|
||||
export { nodes } from './nodes';
|
||||
export { userNodes } from './user-nodes';
|
||||
export { channelMetadata } from './channel-metadata';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { sqliteTable, text, unique } from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { users } from './users';
|
||||
import { nodes } from './nodes';
|
||||
|
||||
export const channelMetadata = sqliteTable(
|
||||
'channel_metadata',
|
||||
{
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
user_id: text('user_id')
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: 'cascade' }),
|
||||
node_id: text('node_id')
|
||||
.notNull()
|
||||
.references(() => nodes.id, { onDelete: 'cascade' }),
|
||||
channel_id: text('channel_id').notNull(),
|
||||
note: text('note').notNull(),
|
||||
created_at: text('created_at')
|
||||
.notNull()
|
||||
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
||||
updated_at: text('updated_at')
|
||||
.notNull()
|
||||
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
||||
},
|
||||
t => [unique().on(t.user_id, t.node_id, t.channel_id)]
|
||||
);
|
||||
|
|
@ -2,3 +2,4 @@ export { teams } from './teams';
|
|||
export { users } from './users';
|
||||
export { nodes } from './nodes';
|
||||
export { userNodes } from './user-nodes';
|
||||
export { channelMetadata } from './channel-metadata';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { DataloaderService } from './dataloader.service';
|
||||
import { AmbossModule } from '../api/amboss/amboss.module';
|
||||
import { ChannelMetadataService } from '../api/channels/channel-metadata.service';
|
||||
|
||||
@Module({
|
||||
imports: [AmbossModule],
|
||||
providers: [DataloaderService],
|
||||
providers: [DataloaderService, ChannelMetadataService],
|
||||
exports: [DataloaderService],
|
||||
})
|
||||
export class DataloaderModule {}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,26 @@ import { Injectable } from '@nestjs/common';
|
|||
import DataLoader from 'dataloader';
|
||||
import { AmbossService } from '../api/amboss/amboss.service';
|
||||
import { EdgeInfo, NodeAlias } from '../api/amboss/amboss.types';
|
||||
import { ChannelMetadataService } from '../api/channels/channel-metadata.service';
|
||||
|
||||
export type ChannelNoteKey = {
|
||||
userId: string;
|
||||
nodeId: string;
|
||||
channelId: string;
|
||||
};
|
||||
|
||||
export type DataloaderTypes = {
|
||||
nodesLoader: DataLoader<string, NodeAlias>;
|
||||
edgesLoader: DataLoader<string, EdgeInfo>;
|
||||
channelNotesLoader: DataLoader<ChannelNoteKey, string | null>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class DataloaderService {
|
||||
constructor(private ambossService: AmbossService) {}
|
||||
constructor(
|
||||
private ambossService: AmbossService,
|
||||
private channelMetadataService: ChannelMetadataService
|
||||
) {}
|
||||
|
||||
createLoaders(): DataloaderTypes {
|
||||
const nodesLoader = new DataLoader<string, NodeAlias>(
|
||||
|
|
@ -21,9 +32,45 @@ export class DataloaderService {
|
|||
async (ids: string[]) => this.ambossService.getEdgeInfoBatch(ids)
|
||||
);
|
||||
|
||||
const channelNotesLoader = new DataLoader<
|
||||
ChannelNoteKey,
|
||||
string | null,
|
||||
string
|
||||
>(
|
||||
async (keys: readonly ChannelNoteKey[]) => {
|
||||
const grouped = new Map<string, ChannelNoteKey[]>();
|
||||
for (const key of keys) {
|
||||
const groupKey = `${key.userId}:${key.nodeId}`;
|
||||
const group = grouped.get(groupKey) || [];
|
||||
group.push(key);
|
||||
grouped.set(groupKey, group);
|
||||
}
|
||||
|
||||
const noteMaps = new Map<string, Map<string, string>>();
|
||||
for (const [groupKey, group] of grouped) {
|
||||
const { userId, nodeId } = group[0];
|
||||
const map = await this.channelMetadataService.getNotesByNode(
|
||||
userId,
|
||||
nodeId
|
||||
);
|
||||
noteMaps.set(groupKey, map);
|
||||
}
|
||||
|
||||
return keys.map(key => {
|
||||
const groupKey = `${key.userId}:${key.nodeId}`;
|
||||
return noteMaps.get(groupKey)?.get(key.channelId) ?? null;
|
||||
});
|
||||
},
|
||||
{
|
||||
cacheKeyFn: (key: ChannelNoteKey) =>
|
||||
`${key.userId}:${key.nodeId}:${key.channelId}`,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
nodesLoader,
|
||||
edgesLoader,
|
||||
channelNotesLoader,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue