refactor(core): simplify integration credentials (#31968)
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { NodeHttpServer } from "@effect/platform-node"
|
import { NodeHttpServer } from "@effect/platform-node"
|
||||||
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
import { PermissionSaved } from "@opencode-ai/core/permission/saved"
|
||||||
import { Context, Layer, Option } from "effect"
|
import { Context, Layer, Option } from "effect"
|
||||||
import * as Effect from "effect/Effect"
|
import * as Effect from "effect/Effect"
|
||||||
@@ -35,6 +36,7 @@ function bind(hostname: string, port: number, password: string) {
|
|||||||
return Layer.build(
|
return Layer.build(
|
||||||
HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
|
HttpRouter.serve(createRoutes(password), { disableListenLog: true, disableLogger: true }).pipe(
|
||||||
Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })),
|
Layer.provideMerge(NodeHttpServer.layer(() => createServer(), { port, host: hostname })),
|
||||||
|
Layer.provide(Credential.defaultLayer),
|
||||||
Layer.provide(PermissionSaved.defaultLayer),
|
Layer.provide(PermissionSaved.defaultLayer),
|
||||||
),
|
),
|
||||||
).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address))
|
).pipe(Effect.map((context) => Context.get(context, HttpServer.HttpServer).address))
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
CREATE TABLE `project` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`worktree` text NOT NULL,
|
|
||||||
`vcs` text,
|
|
||||||
`name` text,
|
|
||||||
`icon_url` text,
|
|
||||||
`icon_color` text,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL,
|
|
||||||
`time_initialized` integer,
|
|
||||||
`sandboxes` text NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `message` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`session_id` text NOT NULL,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL,
|
|
||||||
`data` text NOT NULL,
|
|
||||||
CONSTRAINT `fk_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `part` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`message_id` text NOT NULL,
|
|
||||||
`session_id` text NOT NULL,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL,
|
|
||||||
`data` text NOT NULL,
|
|
||||||
CONSTRAINT `fk_part_message_id_message_id_fk` FOREIGN KEY (`message_id`) REFERENCES `message`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `permission` (
|
|
||||||
`project_id` text PRIMARY KEY,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL,
|
|
||||||
`data` text NOT NULL,
|
|
||||||
CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `session` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`project_id` text NOT NULL,
|
|
||||||
`parent_id` text,
|
|
||||||
`slug` text NOT NULL,
|
|
||||||
`directory` text NOT NULL,
|
|
||||||
`title` text NOT NULL,
|
|
||||||
`version` text NOT NULL,
|
|
||||||
`share_url` text,
|
|
||||||
`summary_additions` integer,
|
|
||||||
`summary_deletions` integer,
|
|
||||||
`summary_files` integer,
|
|
||||||
`summary_diffs` text,
|
|
||||||
`revert` text,
|
|
||||||
`permission` text,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL,
|
|
||||||
`time_compacting` integer,
|
|
||||||
`time_archived` integer,
|
|
||||||
CONSTRAINT `fk_session_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `todo` (
|
|
||||||
`session_id` text NOT NULL,
|
|
||||||
`content` text NOT NULL,
|
|
||||||
`status` text NOT NULL,
|
|
||||||
`priority` text NOT NULL,
|
|
||||||
`position` integer NOT NULL,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL,
|
|
||||||
CONSTRAINT `todo_pk` PRIMARY KEY(`session_id`, `position`),
|
|
||||||
CONSTRAINT `fk_todo_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `session_share` (
|
|
||||||
`session_id` text PRIMARY KEY,
|
|
||||||
`id` text NOT NULL,
|
|
||||||
`secret` text NOT NULL,
|
|
||||||
`url` text NOT NULL,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL,
|
|
||||||
CONSTRAINT `fk_session_share_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE INDEX `message_session_idx` ON `message` (`session_id`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `part_message_idx` ON `part` (`message_id`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `part_session_idx` ON `part` (`session_id`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_project_idx` ON `session` (`project_id`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_parent_idx` ON `session` (`parent_id`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `todo_session_idx` ON `todo` (`session_id`);
|
|
||||||
@@ -1,796 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "068758ed-a97a-46f6-8a59-6c639ae7c20c",
|
|
||||||
"prevIds": ["00000000-0000-0000-0000-000000000000"],
|
|
||||||
"ddl": [
|
|
||||||
{
|
|
||||||
"name": "project",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "message",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "part",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "permission",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "session",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "todo",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "session_share",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "worktree",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "vcs",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "name",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "icon_url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "icon_color",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_initialized",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "sandboxes",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "message_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "project_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "project_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "parent_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "slug",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "directory",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "title",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "version",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "share_url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_additions",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_deletions",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_files",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_diffs",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "revert",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "permission",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_compacting",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_archived",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "content",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "status",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "priority",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "position",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "secret",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_message_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["message_id"],
|
|
||||||
"tableTo": "message",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_part_message_id_message_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"tableTo": "project",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_permission_project_id_project_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"tableTo": "project",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_session_project_id_project_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_todo_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_session_share_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id", "position"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "todo_pk",
|
|
||||||
"entityType": "pks",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "project_pk",
|
|
||||||
"table": "project",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "message_pk",
|
|
||||||
"table": "message",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "part_pk",
|
|
||||||
"table": "part",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "permission_pk",
|
|
||||||
"table": "permission",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "session_pk",
|
|
||||||
"table": "session",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "session_share_pk",
|
|
||||||
"table": "session_share",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "message_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "message_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "part_message_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "part_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "project_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "session_project_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "parent_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "session_parent_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "todo_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "todo"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"renames": []
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE `project` ADD `commands` text;
|
|
||||||
@@ -1,806 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "8bc2d11d-97fa-4ba8-8bfa-6c5956c49aeb",
|
|
||||||
"prevIds": ["068758ed-a97a-46f6-8a59-6c639ae7c20c"],
|
|
||||||
"ddl": [
|
|
||||||
{
|
|
||||||
"name": "project",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "message",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "part",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "permission",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "session",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "todo",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "session_share",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "worktree",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "vcs",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "name",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "icon_url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "icon_color",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_initialized",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "sandboxes",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "commands",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "message_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "project_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "project_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "parent_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "slug",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "directory",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "title",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "version",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "share_url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_additions",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_deletions",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_files",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_diffs",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "revert",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "permission",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_compacting",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_archived",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "content",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "status",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "priority",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "position",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "secret",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_message_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["message_id"],
|
|
||||||
"tableTo": "message",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_part_message_id_message_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"tableTo": "project",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_permission_project_id_project_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"tableTo": "project",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_session_project_id_project_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_todo_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_session_share_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id", "position"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "todo_pk",
|
|
||||||
"entityType": "pks",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "project_pk",
|
|
||||||
"table": "project",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "message_pk",
|
|
||||||
"table": "message",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "part_pk",
|
|
||||||
"table": "part",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "permission_pk",
|
|
||||||
"table": "permission",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "session_pk",
|
|
||||||
"table": "session",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "session_share_pk",
|
|
||||||
"table": "session_share",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "message_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "message_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "part_message_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "part_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "project_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "session_project_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "parent_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "session_parent_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "todo_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "todo"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"renames": []
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
CREATE TABLE `control_account` (
|
|
||||||
`email` text NOT NULL,
|
|
||||||
`url` text NOT NULL,
|
|
||||||
`access_token` text NOT NULL,
|
|
||||||
`refresh_token` text NOT NULL,
|
|
||||||
`token_expiry` integer,
|
|
||||||
`active` integer NOT NULL,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL,
|
|
||||||
CONSTRAINT `control_account_pk` PRIMARY KEY(`email`, `url`)
|
|
||||||
);
|
|
||||||
@@ -1,897 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "d2736e43-700f-4e9e-8151-9f2f0d967bc8",
|
|
||||||
"prevIds": ["8bc2d11d-97fa-4ba8-8bfa-6c5956c49aeb"],
|
|
||||||
"ddl": [
|
|
||||||
{
|
|
||||||
"name": "control_account",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "project",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "message",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "part",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "permission",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "session",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "todo",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "session_share",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "email",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "access_token",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "refresh_token",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "token_expiry",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "active",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "worktree",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "vcs",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "name",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "icon_url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "icon_color",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_initialized",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "sandboxes",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "commands",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "message_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "project_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "project_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "parent_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "slug",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "directory",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "title",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "version",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "share_url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_additions",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_deletions",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_files",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_diffs",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "revert",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "permission",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_compacting",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_archived",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "content",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "status",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "priority",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "position",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "secret",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_message_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["message_id"],
|
|
||||||
"tableTo": "message",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_part_message_id_message_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"tableTo": "project",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_permission_project_id_project_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"tableTo": "project",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_session_project_id_project_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_todo_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_session_share_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["email", "url"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "control_account_pk",
|
|
||||||
"entityType": "pks",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id", "position"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "todo_pk",
|
|
||||||
"entityType": "pks",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "project_pk",
|
|
||||||
"table": "project",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "message_pk",
|
|
||||||
"table": "message",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "part_pk",
|
|
||||||
"table": "part",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "permission_pk",
|
|
||||||
"table": "permission",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "session_pk",
|
|
||||||
"table": "session",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "session_share_pk",
|
|
||||||
"table": "session_share",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "message_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "message_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "part_message_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "part_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "project_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "session_project_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "parent_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "session_parent_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "todo_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "todo"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"renames": []
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
CREATE TABLE `workspace` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`branch` text,
|
|
||||||
`project_id` text NOT NULL,
|
|
||||||
`config` text NOT NULL,
|
|
||||||
CONSTRAINT `fk_workspace_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
@@ -1,959 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "1f1dbf2d-bf66-4b25-8af4-4ba7633b7e40",
|
|
||||||
"prevIds": ["d2736e43-700f-4e9e-8151-9f2f0d967bc8"],
|
|
||||||
"ddl": [
|
|
||||||
{
|
|
||||||
"name": "workspace",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "control_account",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "project",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "message",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "part",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "permission",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "session",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "todo",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "session_share",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "workspace"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "branch",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "workspace"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "project_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "workspace"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "config",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "workspace"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "email",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "access_token",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "refresh_token",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "token_expiry",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "active",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "worktree",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "vcs",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "name",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "icon_url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "icon_color",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_initialized",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "sandboxes",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "commands",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "message_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "project_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "project_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "parent_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "slug",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "directory",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "title",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "version",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "share_url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_additions",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_deletions",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_files",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_diffs",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "revert",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "permission",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_compacting",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_archived",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "content",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "status",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "priority",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "position",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "secret",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"tableTo": "project",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_workspace_project_id_project_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "workspace"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_message_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["message_id"],
|
|
||||||
"tableTo": "message",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_part_message_id_message_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"tableTo": "project",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_permission_project_id_project_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"tableTo": "project",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_session_project_id_project_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_todo_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_session_share_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["email", "url"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "control_account_pk",
|
|
||||||
"entityType": "pks",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id", "position"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "todo_pk",
|
|
||||||
"entityType": "pks",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "workspace_pk",
|
|
||||||
"table": "workspace",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "project_pk",
|
|
||||||
"table": "project",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "message_pk",
|
|
||||||
"table": "message",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "part_pk",
|
|
||||||
"table": "part",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "permission_pk",
|
|
||||||
"table": "permission",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "session_pk",
|
|
||||||
"table": "session",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "session_share_pk",
|
|
||||||
"table": "session_share",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "message_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "message_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "part_message_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "part_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "project_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "session_project_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "parent_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "session_parent_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "todo_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "todo"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"renames": []
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE `session` ADD `workspace_id` text;--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_workspace_idx` ON `session` (`workspace_id`);
|
|
||||||
-983
@@ -1,983 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "7",
|
|
||||||
"dialect": "sqlite",
|
|
||||||
"id": "572fb732-56f4-4b1e-b981-77152c9980dd",
|
|
||||||
"prevIds": ["1f1dbf2d-bf66-4b25-8af4-4ba7633b7e40"],
|
|
||||||
"ddl": [
|
|
||||||
{
|
|
||||||
"name": "workspace",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "control_account",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "project",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "message",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "part",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "permission",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "session",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "todo",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "session_share",
|
|
||||||
"entityType": "tables"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "workspace"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "branch",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "workspace"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "project_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "workspace"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "config",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "workspace"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "email",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "access_token",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "refresh_token",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "token_expiry",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "active",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "worktree",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "vcs",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "name",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "icon_url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "icon_color",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_initialized",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "sandboxes",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "commands",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "message_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "project_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "data",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "project_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "workspace_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "parent_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "slug",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "directory",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "title",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "version",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "share_url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_additions",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_deletions",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_files",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "summary_diffs",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "revert",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "permission",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_compacting",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_archived",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "content",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "status",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "priority",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "position",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": false,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "session_id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "id",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "secret",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "url",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_created",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "time_updated",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"tableTo": "project",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_workspace_project_id_project_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "workspace"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_message_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["message_id"],
|
|
||||||
"tableTo": "message",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_part_message_id_message_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"tableTo": "project",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_permission_project_id_project_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "permission"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"tableTo": "project",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_session_project_id_project_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_todo_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"tableTo": "session",
|
|
||||||
"columnsTo": ["id"],
|
|
||||||
"onUpdate": "NO ACTION",
|
|
||||||
"onDelete": "CASCADE",
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "fk_session_share_session_id_session_id_fk",
|
|
||||||
"entityType": "fks",
|
|
||||||
"table": "session_share"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["email", "url"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "control_account_pk",
|
|
||||||
"entityType": "pks",
|
|
||||||
"table": "control_account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id", "position"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "todo_pk",
|
|
||||||
"entityType": "pks",
|
|
||||||
"table": "todo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "workspace_pk",
|
|
||||||
"table": "workspace",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "project_pk",
|
|
||||||
"table": "project",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "message_pk",
|
|
||||||
"table": "message",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "part_pk",
|
|
||||||
"table": "part",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["project_id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "permission_pk",
|
|
||||||
"table": "permission",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "session_pk",
|
|
||||||
"table": "session",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": ["session_id"],
|
|
||||||
"nameExplicit": false,
|
|
||||||
"name": "session_share_pk",
|
|
||||||
"table": "session_share",
|
|
||||||
"entityType": "pks"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "message_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "message"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "message_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "part_message_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "part_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "part"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "project_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "session_project_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "workspace_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "session_workspace_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "parent_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "session_parent_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "session"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "session_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": false,
|
|
||||||
"where": null,
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "todo_session_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "todo"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"renames": []
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
CREATE TABLE `account` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`email` text NOT NULL,
|
|
||||||
`url` text NOT NULL,
|
|
||||||
`access_token` text NOT NULL,
|
|
||||||
`refresh_token` text NOT NULL,
|
|
||||||
`token_expiry` integer,
|
|
||||||
`selected_org_id` text,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `account_state` (
|
|
||||||
`id` integer PRIMARY KEY NOT NULL,
|
|
||||||
`active_account_id` text,
|
|
||||||
FOREIGN KEY (`active_account_id`) REFERENCES `account`(`id`) ON UPDATE no action ON DELETE set null
|
|
||||||
);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
|||||||
ALTER TABLE `workspace` ADD `type` text NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `workspace` ADD `name` text;--> statement-breakpoint
|
|
||||||
ALTER TABLE `workspace` ADD `directory` text;--> statement-breakpoint
|
|
||||||
ALTER TABLE `workspace` ADD `extra` text;--> statement-breakpoint
|
|
||||||
ALTER TABLE `workspace` DROP COLUMN `config`;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +0,0 @@
|
|||||||
ALTER TABLE `account_state` ADD `active_org_id` text;--> statement-breakpoint
|
|
||||||
UPDATE `account_state` SET `active_org_id` = (SELECT `selected_org_id` FROM `account` WHERE `account`.`id` = `account_state`.`active_account_id`);--> statement-breakpoint
|
|
||||||
ALTER TABLE `account` DROP COLUMN `selected_org_id`;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS `message_session_idx`;--> statement-breakpoint
|
|
||||||
DROP INDEX IF EXISTS `part_message_idx`;--> statement-breakpoint
|
|
||||||
CREATE INDEX `message_session_time_created_id_idx` ON `message` (`session_id`,`time_created`,`id`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `part_message_id_id_idx` ON `part` (`message_id`,`id`);
|
|
||||||
-1168
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
|||||||
CREATE TABLE `event_sequence` (
|
|
||||||
`aggregate_id` text PRIMARY KEY,
|
|
||||||
`seq` integer NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE `event` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`aggregate_id` text NOT NULL,
|
|
||||||
`seq` integer NOT NULL,
|
|
||||||
`type` text NOT NULL,
|
|
||||||
`data` text NOT NULL,
|
|
||||||
CONSTRAINT `fk_event_aggregate_id_event_sequence_aggregate_id_fk` FOREIGN KEY (`aggregate_id`) REFERENCES `event_sequence`(`aggregate_id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
-1271
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
|||||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
|
||||||
CREATE TABLE `__new_workspace` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`type` text NOT NULL,
|
|
||||||
`name` text DEFAULT '' NOT NULL,
|
|
||||||
`branch` text,
|
|
||||||
`directory` text,
|
|
||||||
`extra` text,
|
|
||||||
`project_id` text NOT NULL,
|
|
||||||
CONSTRAINT `fk_workspace_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
INSERT INTO `__new_workspace`(`id`, `type`, `branch`, `name`, `directory`, `extra`, `project_id`) SELECT `id`, `type`, `branch`, `name`, `directory`, `extra`, `project_id` FROM `workspace`;--> statement-breakpoint
|
|
||||||
DROP TABLE `workspace`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `__new_workspace` RENAME TO `workspace`;--> statement-breakpoint
|
|
||||||
PRAGMA foreign_keys=ON;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
|||||||
CREATE TABLE `session_entry` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`session_id` text NOT NULL,
|
|
||||||
`type` text NOT NULL,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL,
|
|
||||||
`data` text NOT NULL,
|
|
||||||
CONSTRAINT `fk_session_entry_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_entry_session_idx` ON `session_entry` (`session_id`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_entry_session_type_idx` ON `session_entry` (`session_id`,`type`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_entry_time_created_idx` ON `session_entry` (`time_created`);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE `project` ADD `icon_url_override` text;
|
|
||||||
UPDATE `project` SET `icon_url_override` = `icon_url` WHERE `icon_url` IS NOT NULL;
|
|
||||||
-1409
File diff suppressed because it is too large
Load Diff
@@ -1,17 +0,0 @@
|
|||||||
CREATE TABLE `session_message` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`session_id` text NOT NULL,
|
|
||||||
`type` text NOT NULL,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL,
|
|
||||||
`data` text NOT NULL,
|
|
||||||
CONSTRAINT `fk_session_message_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
DROP INDEX IF EXISTS `session_entry_session_idx`;--> statement-breakpoint
|
|
||||||
DROP INDEX IF EXISTS `session_entry_session_type_idx`;--> statement-breakpoint
|
|
||||||
DROP INDEX IF EXISTS `session_entry_time_created_idx`;--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_message_session_idx` ON `session_message` (`session_id`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_message_session_type_idx` ON `session_message` (`session_id`,`type`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_message_time_created_idx` ON `session_message` (`time_created`);--> statement-breakpoint
|
|
||||||
DROP TABLE `session_entry`;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE `session` ADD `path` text;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
|||||||
ALTER TABLE `session` ADD `agent` text;--> statement-breakpoint
|
|
||||||
ALTER TABLE `session` ADD `model` text;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE `event_sequence` ADD `owner_id` text;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE `workspace` ADD `time_used` integer NOT NULL DEFAULT 0;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
|||||||
ALTER TABLE `session` ADD `cost` real DEFAULT 0 NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `session` ADD `tokens_input` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `session` ADD `tokens_output` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `session` ADD `tokens_reasoning` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `session` ADD `tokens_cache_read` integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
|
||||||
ALTER TABLE `session` ADD `tokens_cache_write` integer DEFAULT 0 NOT NULL;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
|||||||
CREATE TABLE `data_migration` (
|
|
||||||
`name` text PRIMARY KEY,
|
|
||||||
`time_completed` integer NOT NULL
|
|
||||||
);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE `session` ADD `metadata` text;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +0,0 @@
|
|||||||
UPDATE project SET worktree = REPLACE(worktree, char(92), '/') WHERE worktree GLOB '[A-Za-z]:' || char(92) || '*' OR worktree LIKE char(92) || char(92) || '%';
|
|
||||||
--> statement-breakpoint
|
|
||||||
UPDATE project SET sandboxes = REPLACE(sandboxes, char(92) || char(92), '/') WHERE instr(sandboxes, char(92)) > 0 AND (worktree GLOB '[A-Za-z]:*' OR worktree LIKE '//%');
|
|
||||||
--> statement-breakpoint
|
|
||||||
UPDATE session SET directory = REPLACE(directory, char(92), '/') WHERE directory GLOB '[A-Za-z]:' || char(92) || '*' OR directory LIKE char(92) || char(92) || '%';
|
|
||||||
--> statement-breakpoint
|
|
||||||
UPDATE session SET path = REPLACE(path, char(92), '/') WHERE path IS NOT NULL AND instr(path, char(92)) > 0 AND (directory GLOB '[A-Za-z]:*' OR directory LIKE '//%');
|
|
||||||
-1560
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
DROP TABLE `permission`;
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
|||||||
CREATE TABLE `permission` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`project_id` text NOT NULL,
|
|
||||||
`action` text NOT NULL,
|
|
||||||
`resource` text NOT NULL,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL,
|
|
||||||
CONSTRAINT `fk_permission_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `permission_project_action_resource_idx` ON `permission` (`project_id`,`action`,`resource`);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
|||||||
CREATE TABLE `project_directory` (
|
|
||||||
`project_id` text NOT NULL,
|
|
||||||
`directory` text NOT NULL,
|
|
||||||
`type` text NOT NULL,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
CONSTRAINT `project_directory_pk` PRIMARY KEY(`project_id`, `directory`),
|
|
||||||
CONSTRAINT `fk_project_directory_project_id_project_id_fk` FOREIGN KEY (`project_id`) REFERENCES `project`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
-1664
File diff suppressed because it is too large
Load Diff
-5
@@ -1,5 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS `session_message_session_idx`;--> statement-breakpoint
|
|
||||||
DROP INDEX IF EXISTS `session_message_session_type_idx`;--> statement-breakpoint
|
|
||||||
CREATE INDEX `event_aggregate_seq_idx` ON `event` (`aggregate_id`,`seq`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_message_session_time_created_id_idx` ON `session_message` (`session_id`,`time_created`,`id`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_message_session_type_time_created_id_idx` ON `session_message` (`session_id`,`type`,`time_created`,`id`);
|
|
||||||
-1636
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
|||||||
DELETE FROM `session_message`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `session_message` ADD `seq` integer NOT NULL;--> statement-breakpoint
|
|
||||||
DROP INDEX IF EXISTS `session_message_session_time_created_id_idx`;--> statement-breakpoint
|
|
||||||
DROP INDEX IF EXISTS `session_message_session_type_time_created_id_idx`;--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_message_session_seq_idx` ON `session_message` (`session_id`,`seq`);--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_message_session_type_seq_idx` ON `session_message` (`session_id`,`type`,`seq`);
|
|
||||||
-1638
File diff suppressed because it is too large
Load Diff
@@ -1,12 +0,0 @@
|
|||||||
CREATE TABLE `session_input` (
|
|
||||||
`seq` integer PRIMARY KEY AUTOINCREMENT,
|
|
||||||
`id` text NOT NULL UNIQUE,
|
|
||||||
`session_id` text NOT NULL,
|
|
||||||
`prompt` text NOT NULL,
|
|
||||||
`delivery` text NOT NULL,
|
|
||||||
`promoted_seq` integer,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
CONSTRAINT `fk_session_input_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_input_session_pending_seq_idx` ON `session_input` (`session_id`,`promoted_seq`,`seq`);
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
|||||||
DROP INDEX IF EXISTS `session_input_session_pending_seq_idx`;--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS `event_aggregate_type_seq_idx` ON `event` (`aggregate_id`,`type`,`seq`);--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS `session_input_session_pending_delivery_seq_idx` ON `session_input` (`session_id`,`promoted_seq`,`delivery`,`seq`);--> statement-breakpoint
|
|
||||||
CREATE INDEX IF NOT EXISTS `session_message_session_time_created_id_idx` ON `session_message` (`session_id`,`time_created`,`id`);
|
|
||||||
-1869
File diff suppressed because it is too large
Load Diff
@@ -1,28 +0,0 @@
|
|||||||
DELETE FROM `session_input`;--> statement-breakpoint
|
|
||||||
DELETE FROM `session_message`;--> statement-breakpoint
|
|
||||||
DELETE FROM `event`;--> statement-breakpoint
|
|
||||||
DELETE FROM `event_sequence`;--> statement-breakpoint
|
|
||||||
UPDATE `session` SET `workspace_id` = NULL;--> statement-breakpoint
|
|
||||||
DELETE FROM `workspace`;--> statement-breakpoint
|
|
||||||
DROP INDEX IF EXISTS `event_aggregate_seq_idx`;--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `event_aggregate_seq_idx` ON `event` (`aggregate_id`,`seq`);--> statement-breakpoint
|
|
||||||
DROP INDEX IF EXISTS `session_message_session_seq_idx`;--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `session_message_session_seq_idx` ON `session_message` (`session_id`,`seq`);--> statement-breakpoint
|
|
||||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
|
||||||
CREATE TABLE `__new_session_input` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`session_id` text NOT NULL,
|
|
||||||
`prompt` text NOT NULL,
|
|
||||||
`delivery` text NOT NULL,
|
|
||||||
`admitted_seq` integer NOT NULL,
|
|
||||||
`promoted_seq` integer,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
CONSTRAINT `fk_session_input_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
DROP TABLE `session_input`;--> statement-breakpoint
|
|
||||||
ALTER TABLE `__new_session_input` RENAME TO `session_input`;--> statement-breakpoint
|
|
||||||
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
|
||||||
CREATE INDEX `session_input_session_pending_delivery_seq_idx` ON `session_input` (`session_id`,`promoted_seq`,`delivery`,`admitted_seq`);--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `session_input_session_admitted_seq_idx` ON `session_input` (`session_id`,`admitted_seq`);--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `session_input_session_promoted_seq_idx` ON `session_input` (`session_id`,`promoted_seq`);
|
|
||||||
-1898
File diff suppressed because it is too large
Load Diff
@@ -1,9 +0,0 @@
|
|||||||
CREATE TABLE `session_context_epoch` (
|
|
||||||
`session_id` text PRIMARY KEY,
|
|
||||||
`baseline` text NOT NULL,
|
|
||||||
`snapshot` text NOT NULL,
|
|
||||||
`baseline_seq` integer NOT NULL,
|
|
||||||
`replacement_seq` integer,
|
|
||||||
`revision` integer DEFAULT 0 NOT NULL,
|
|
||||||
CONSTRAINT `fk_session_context_epoch_session_id_session_id_fk` FOREIGN KEY (`session_id`) REFERENCES `session`(`id`) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
-1980
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
ALTER TABLE `session_context_epoch` ADD `agent` text DEFAULT 'build' NOT NULL;
|
|
||||||
-1990
File diff suppressed because it is too large
Load Diff
@@ -1,12 +0,0 @@
|
|||||||
CREATE TABLE `credential` (
|
|
||||||
`id` text PRIMARY KEY,
|
|
||||||
`connector_id` text NOT NULL,
|
|
||||||
`method_id` text NOT NULL,
|
|
||||||
`label` text NOT NULL,
|
|
||||||
`value` text NOT NULL,
|
|
||||||
`active` integer DEFAULT false NOT NULL,
|
|
||||||
`time_created` integer NOT NULL,
|
|
||||||
`time_updated` integer NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE UNIQUE INDEX `credential_connector_active_idx` ON `credential` (`connector_id`) WHERE "credential"."active" = 1;
|
|
||||||
+143
-82
@@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "sqlite",
|
"dialect": "sqlite",
|
||||||
"id": "f25f9126-c7dc-4882-9ff4-af27e11d2da1",
|
"id": "abd2f920-b822-49af-b8a7-2e48367d424f",
|
||||||
"prevIds": ["d1bfa125-b81e-4c61-9b6e-e74abf6e488f"],
|
"prevIds": [
|
||||||
|
"f25f9126-c7dc-4882-9ff4-af27e11d2da1"
|
||||||
|
],
|
||||||
"ddl": [
|
"ddl": [
|
||||||
{
|
{
|
||||||
"name": "workspace",
|
"name": "workspace",
|
||||||
@@ -386,17 +388,7 @@
|
|||||||
"autoincrement": false,
|
"autoincrement": false,
|
||||||
"default": null,
|
"default": null,
|
||||||
"generated": null,
|
"generated": null,
|
||||||
"name": "connector_id",
|
"name": "integration_id",
|
||||||
"entityType": "columns",
|
|
||||||
"table": "credential"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": null,
|
|
||||||
"generated": null,
|
|
||||||
"name": "method_id",
|
|
||||||
"entityType": "columns",
|
"entityType": "columns",
|
||||||
"table": "credential"
|
"table": "credential"
|
||||||
},
|
},
|
||||||
@@ -420,16 +412,6 @@
|
|||||||
"entityType": "columns",
|
"entityType": "columns",
|
||||||
"table": "credential"
|
"table": "credential"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "integer",
|
|
||||||
"notNull": true,
|
|
||||||
"autoincrement": false,
|
|
||||||
"default": "false",
|
|
||||||
"generated": null,
|
|
||||||
"name": "active",
|
|
||||||
"entityType": "columns",
|
|
||||||
"table": "credential"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"notNull": true,
|
"notNull": true,
|
||||||
@@ -1491,9 +1473,13 @@
|
|||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id"],
|
"columns": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1502,9 +1488,13 @@
|
|||||||
"table": "workspace"
|
"table": "workspace"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["active_account_id"],
|
"columns": [
|
||||||
|
"active_account_id"
|
||||||
|
],
|
||||||
"tableTo": "account",
|
"tableTo": "account",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "SET NULL",
|
"onDelete": "SET NULL",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1513,9 +1503,13 @@
|
|||||||
"table": "account_state"
|
"table": "account_state"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["aggregate_id"],
|
"columns": [
|
||||||
|
"aggregate_id"
|
||||||
|
],
|
||||||
"tableTo": "event_sequence",
|
"tableTo": "event_sequence",
|
||||||
"columnsTo": ["aggregate_id"],
|
"columnsTo": [
|
||||||
|
"aggregate_id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1524,9 +1518,13 @@
|
|||||||
"table": "event"
|
"table": "event"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id"],
|
"columns": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1535,9 +1533,13 @@
|
|||||||
"table": "permission"
|
"table": "permission"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id"],
|
"columns": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1546,9 +1548,13 @@
|
|||||||
"table": "project_directory"
|
"table": "project_directory"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1557,9 +1563,13 @@
|
|||||||
"table": "message"
|
"table": "message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["message_id"],
|
"columns": [
|
||||||
|
"message_id"
|
||||||
|
],
|
||||||
"tableTo": "message",
|
"tableTo": "message",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1568,9 +1578,13 @@
|
|||||||
"table": "part"
|
"table": "part"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1579,9 +1593,13 @@
|
|||||||
"table": "session_context_epoch"
|
"table": "session_context_epoch"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1590,9 +1608,13 @@
|
|||||||
"table": "session_input"
|
"table": "session_input"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1601,9 +1623,13 @@
|
|||||||
"table": "session_message"
|
"table": "session_message"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id"],
|
"columns": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
"tableTo": "project",
|
"tableTo": "project",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1612,9 +1638,13 @@
|
|||||||
"table": "session"
|
"table": "session"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1623,9 +1653,13 @@
|
|||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"tableTo": "session",
|
"tableTo": "session",
|
||||||
"columnsTo": ["id"],
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"onUpdate": "NO ACTION",
|
"onUpdate": "NO ACTION",
|
||||||
"onDelete": "CASCADE",
|
"onDelete": "CASCADE",
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
@@ -1634,152 +1668,179 @@
|
|||||||
"table": "session_share"
|
"table": "session_share"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["email", "url"],
|
"columns": [
|
||||||
|
"email",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "control_account_pk",
|
"name": "control_account_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "control_account"
|
"table": "control_account"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["project_id", "directory"],
|
"columns": [
|
||||||
|
"project_id",
|
||||||
|
"directory"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "project_directory_pk",
|
"name": "project_directory_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "project_directory"
|
"table": "project_directory"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id", "position"],
|
"columns": [
|
||||||
|
"session_id",
|
||||||
|
"position"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "todo_pk",
|
"name": "todo_pk",
|
||||||
"entityType": "pks",
|
"entityType": "pks",
|
||||||
"table": "todo"
|
"table": "todo"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "workspace_pk",
|
"name": "workspace_pk",
|
||||||
"table": "workspace",
|
"table": "workspace",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["name"],
|
"columns": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "data_migration_pk",
|
"name": "data_migration_pk",
|
||||||
"table": "data_migration",
|
"table": "data_migration",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_state_pk",
|
"name": "account_state_pk",
|
||||||
"table": "account_state",
|
"table": "account_state",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "account_pk",
|
"name": "account_pk",
|
||||||
"table": "account",
|
"table": "account",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "credential_pk",
|
"name": "credential_pk",
|
||||||
"table": "credential",
|
"table": "credential",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["aggregate_id"],
|
"columns": [
|
||||||
|
"aggregate_id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_sequence_pk",
|
"name": "event_sequence_pk",
|
||||||
"table": "event_sequence",
|
"table": "event_sequence",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "event_pk",
|
"name": "event_pk",
|
||||||
"table": "event",
|
"table": "event",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "permission_pk",
|
"name": "permission_pk",
|
||||||
"table": "permission",
|
"table": "permission",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "project_pk",
|
"name": "project_pk",
|
||||||
"table": "project",
|
"table": "project",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "message_pk",
|
"name": "message_pk",
|
||||||
"table": "message",
|
"table": "message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "part_pk",
|
"name": "part_pk",
|
||||||
"table": "part",
|
"table": "part",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_context_epoch_pk",
|
"name": "session_context_epoch_pk",
|
||||||
"table": "session_context_epoch",
|
"table": "session_context_epoch",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_input_pk",
|
"name": "session_input_pk",
|
||||||
"table": "session_input",
|
"table": "session_input",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_message_pk",
|
"name": "session_message_pk",
|
||||||
"table": "session_message",
|
"table": "session_message",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["id"],
|
"columns": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_pk",
|
"name": "session_pk",
|
||||||
"table": "session",
|
"table": "session",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"columns": ["session_id"],
|
"columns": [
|
||||||
|
"session_id"
|
||||||
|
],
|
||||||
"nameExplicit": false,
|
"nameExplicit": false,
|
||||||
"name": "session_share_pk",
|
"name": "session_share_pk",
|
||||||
"table": "session_share",
|
"table": "session_share",
|
||||||
"entityType": "pks"
|
"entityType": "pks"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"value": "connector_id",
|
|
||||||
"isExpression": false
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"isUnique": true,
|
|
||||||
"where": "\"credential\".\"active\" = 1",
|
|
||||||
"origin": "manual",
|
|
||||||
"name": "credential_connector_active_idx",
|
|
||||||
"entityType": "indexes",
|
|
||||||
"table": "credential"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -8,9 +8,10 @@ import { pathToFileURL } from "url"
|
|||||||
import { parseArgs } from "util"
|
import { parseArgs } from "util"
|
||||||
|
|
||||||
const root = path.resolve(import.meta.dirname, "../../..")
|
const root = path.resolve(import.meta.dirname, "../../..")
|
||||||
const sqlDir = path.join(root, "packages/core/migration")
|
const snapshot = path.join(root, "packages/core/schema.json")
|
||||||
const tsDir = path.join(root, "packages/core/src/database/migration")
|
const tsDir = path.join(root, "packages/core/src/database/migration")
|
||||||
const registry = path.join(root, "packages/core/src/database/migration.gen.ts")
|
const registry = path.join(root, "packages/core/src/database/migration.gen.ts")
|
||||||
|
const schema = path.join(root, "packages/core/src/database/schema.gen.ts")
|
||||||
const args = parseArgs({
|
const args = parseArgs({
|
||||||
args: process.argv.slice(2),
|
args: process.argv.slice(2),
|
||||||
options: {
|
options: {
|
||||||
@@ -24,57 +25,62 @@ if (args.values.check) {
|
|||||||
process.exit(0)
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
await $`bun drizzle-kit generate ${args.values.name ? ["--name", args.values.name] : []}`.cwd(
|
await generate()
|
||||||
path.join(root, "packages/core"),
|
|
||||||
)
|
|
||||||
|
|
||||||
const sqlMigrations = (await Array.fromAsync(new Bun.Glob("*/migration.sql").scan({ cwd: sqlDir })))
|
async function generate() {
|
||||||
.map((file) => file.split("/")[0])
|
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-migration-"))
|
||||||
.filter((name) => name !== undefined)
|
const incremental = path.join(temporary, "incremental")
|
||||||
.sort()
|
const full = path.join(temporary, "full")
|
||||||
|
try {
|
||||||
|
await fs.mkdir(incremental)
|
||||||
|
await fs.mkdir(path.join(incremental, "baseline"))
|
||||||
|
await fs.copyFile(snapshot, path.join(incremental, "baseline/snapshot.json"))
|
||||||
|
await drizzle(temporary, incremental, args.values.name)
|
||||||
|
|
||||||
for (const name of sqlMigrations) {
|
const generated = await generatedMigrations(incremental)
|
||||||
if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
|
if (generated.length > 1) throw new Error(`Expected one generated migration, found ${generated.length}.`)
|
||||||
await Bun.write(
|
const name = generated[0]
|
||||||
path.join(tsDir, `${name}.ts`),
|
if (name) {
|
||||||
renderMigration(name, await Bun.file(path.join(sqlDir, name, "migration.sql")).text()),
|
const target = path.join(tsDir, `${name}.ts`)
|
||||||
)
|
if (await Bun.file(target).exists()) throw new Error(`Database migration already exists: ${name}`)
|
||||||
|
await Bun.write(
|
||||||
|
target,
|
||||||
|
renderMigration(name, await Bun.file(path.join(incremental, name, "migration.sql")).text()),
|
||||||
|
)
|
||||||
|
await fs.copyFile(path.join(incremental, name, "snapshot.json"), snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.mkdir(full)
|
||||||
|
await drizzle(temporary, full, "schema")
|
||||||
|
await Bun.write(schema, renderSchema(await generatedSql(full)))
|
||||||
|
await Bun.write(registry, renderRegistry(await typescriptMigrations()))
|
||||||
|
} finally {
|
||||||
|
await fs.rm(temporary, { recursive: true, force: true })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Bun.write(registry, renderRegistry(sqlMigrations))
|
|
||||||
|
|
||||||
async function check() {
|
async function check() {
|
||||||
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-migration-check-"))
|
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-migration-check-"))
|
||||||
const output = path.join(temporary, "migration")
|
const incremental = path.join(temporary, "incremental")
|
||||||
|
const full = path.join(temporary, "full")
|
||||||
try {
|
try {
|
||||||
await fs.cp(sqlDir, output, { recursive: true })
|
await fs.mkdir(incremental)
|
||||||
const config = path.join(temporary, "drizzle.config.ts")
|
await fs.mkdir(path.join(incremental, "baseline"))
|
||||||
await Bun.write(
|
await fs.copyFile(snapshot, path.join(incremental, "baseline/snapshot.json"))
|
||||||
config,
|
await drizzle(temporary, incremental)
|
||||||
`import config from ${JSON.stringify(pathToFileURL(path.join(root, "packages/core/drizzle.config.ts")).href)}
|
if ((await generatedMigrations(incremental)).length > 0) {
|
||||||
|
|
||||||
export default { ...config, out: ${JSON.stringify(output)} }
|
|
||||||
`,
|
|
||||||
)
|
|
||||||
const before = await snapshot(output)
|
|
||||||
await $`bun drizzle-kit generate --config ${config}`.cwd(path.join(root, "packages/core"))
|
|
||||||
const after = await snapshot(output)
|
|
||||||
if (JSON.stringify(after) !== JSON.stringify(before)) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Core schema has ungenerated database migrations. Run `bun script/migration.ts` from packages/core.",
|
"Core schema has ungenerated database migrations. Run `bun script/migration.ts` from packages/core.",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrations = before
|
await fs.mkdir(full)
|
||||||
.map((entry) => entry.path.split("/")[0])
|
await drizzle(temporary, full, "schema")
|
||||||
.filter((name, index, all) => name !== undefined && all.indexOf(name) === index)
|
if ((await Bun.file(schema).text()) !== renderSchema(await generatedSql(full))) {
|
||||||
.sort()
|
throw new Error("Current database schema is stale. Run `bun script/migration.ts` from packages/core.")
|
||||||
for (const name of migrations) {
|
|
||||||
if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
|
|
||||||
throw new Error(
|
|
||||||
`Database migration TypeScript wrapper is missing for ${name}. Run \`bun script/migration.ts\` from packages/core.`,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const migrations = await typescriptMigrations()
|
||||||
if ((await Bun.file(registry).text()) !== renderRegistry(migrations)) {
|
if ((await Bun.file(registry).text()) !== renderRegistry(migrations)) {
|
||||||
throw new Error("Database migration registry is stale. Run `bun script/migration.ts` from packages/core.")
|
throw new Error("Database migration registry is stale. Run `bun script/migration.ts` from packages/core.")
|
||||||
}
|
}
|
||||||
@@ -83,11 +89,37 @@ export default { ...config, out: ${JSON.stringify(output)} }
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function snapshot(directory: string) {
|
async function drizzle(temporary: string, output: string, name?: string) {
|
||||||
const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: directory, onlyFiles: true }))
|
const config = path.join(temporary, `${path.basename(output)}.config.ts`)
|
||||||
return Promise.all(
|
await Bun.write(
|
||||||
files.sort().map(async (file) => ({ path: file, contents: await Bun.file(path.join(directory, file)).text() })),
|
config,
|
||||||
|
`import config from ${JSON.stringify(pathToFileURL(path.join(root, "packages/core/drizzle.config.ts")).href)}
|
||||||
|
|
||||||
|
export default { ...config, out: ${JSON.stringify(output)} }
|
||||||
|
`,
|
||||||
)
|
)
|
||||||
|
await $`bun drizzle-kit generate --config ${config} ${name ? ["--name", name] : []}`.cwd(
|
||||||
|
path.join(root, "packages/core"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generatedMigrations(directory: string) {
|
||||||
|
return (await Array.fromAsync(new Bun.Glob("*/migration.sql").scan({ cwd: directory })))
|
||||||
|
.map((file) => file.split("/")[0])
|
||||||
|
.filter((name): name is string => name !== undefined)
|
||||||
|
.sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generatedSql(directory: string) {
|
||||||
|
const generated = await generatedMigrations(directory)
|
||||||
|
if (generated.length !== 1) throw new Error(`Expected one full schema migration, found ${generated.length}.`)
|
||||||
|
return Bun.file(path.join(directory, generated[0]!, "migration.sql")).text()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function typescriptMigrations() {
|
||||||
|
return (await Array.fromAsync(new Bun.Glob("*.ts").scan({ cwd: tsDir })))
|
||||||
|
.map((file) => path.basename(file, ".ts"))
|
||||||
|
.sort()
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMigration(name: string, sql: string) {
|
function renderMigration(name: string, sql: string) {
|
||||||
@@ -98,18 +130,36 @@ export default {
|
|||||||
id: ${JSON.stringify(name)},
|
id: ${JSON.stringify(name)},
|
||||||
up(tx) {
|
up(tx) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
${sql
|
${renderStatements(sql)}
|
||||||
.split("--> statement-breakpoint")
|
|
||||||
.map((statement) => statement.trim())
|
|
||||||
.filter((statement) => statement.length > 0)
|
|
||||||
.map(renderRun)
|
|
||||||
.join("\n")}
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
} satisfies DatabaseMigration.Migration
|
} satisfies DatabaseMigration.Migration
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSchema(sql: string) {
|
||||||
|
return `import { Effect } from "effect"
|
||||||
|
import type { DatabaseMigration } from "./migration"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
up(tx) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
${renderStatements(sql)}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} satisfies Omit<DatabaseMigration.Migration, "id">
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStatements(sql: string) {
|
||||||
|
return sql
|
||||||
|
.split("--> statement-breakpoint")
|
||||||
|
.map((statement) => statement.trim())
|
||||||
|
.filter((statement) => statement.length > 0)
|
||||||
|
.map(renderRun)
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
function renderRun(statement: string) {
|
function renderRun(statement: string) {
|
||||||
const lines = statement.replaceAll("\t", " ").split("\n")
|
const lines = statement.replaceAll("\t", " ").split("\n")
|
||||||
if (lines.length === 1) return ` yield* tx.run(\`${escapeTemplate(lines[0])}\`)`
|
if (lines.length === 1) return ` yield* tx.run(\`${escapeTemplate(lines[0])}\`)`
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export * as Catalog from "./catalog"
|
export * as Catalog from "./catalog"
|
||||||
|
|
||||||
import { Context, Effect, Layer, Option, Order, pipe, Schema, Array, Scope, Stream } from "effect"
|
import { Array, Context, Effect, Layer, Option, Order, pipe, Schema, Scope, Stream } from "effect"
|
||||||
import { castDraft, enableMapSet, type Draft } from "immer"
|
import { castDraft, enableMapSet, type Draft } from "immer"
|
||||||
import { ModelV2 } from "./model"
|
import { ModelV2 } from "./model"
|
||||||
import { ModelRequest } from "./model-request"
|
import { ModelRequest } from "./model-request"
|
||||||
@@ -11,7 +11,7 @@ import { EventV2 } from "./event"
|
|||||||
import { Policy } from "./policy"
|
import { Policy } from "./policy"
|
||||||
import { State } from "./state"
|
import { State } from "./state"
|
||||||
import { Credential } from "./credential"
|
import { Credential } from "./credential"
|
||||||
import { ConnectorSchema } from "./connector/schema"
|
import { IntegrationSchema } from "./integration/schema"
|
||||||
|
|
||||||
export type ProviderRecord = {
|
export type ProviderRecord = {
|
||||||
provider: ProviderV2.Info
|
provider: ProviderV2.Info
|
||||||
@@ -99,8 +99,8 @@ export const layer = Layer.effect(
|
|||||||
const credentials = yield* Credential.Service
|
const credentials = yield* Credential.Service
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
|
|
||||||
const project = (provider: ProviderV2.Info, active: Map<ConnectorSchema.ID, Credential.Info>) => {
|
const project = (provider: ProviderV2.Info, active: Map<IntegrationSchema.ID, Credential.Stored>) => {
|
||||||
const credential = active.get(ConnectorSchema.ID.make(provider.id))
|
const credential = active.get(IntegrationSchema.ID.make(provider.id))
|
||||||
if (!credential) return provider
|
if (!credential) return provider
|
||||||
const body = { ...provider.request.body }
|
const body = { ...provider.request.body }
|
||||||
if (credential.value.type === "key") {
|
if (credential.value.type === "key") {
|
||||||
@@ -211,7 +211,9 @@ export const layer = Layer.effect(
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
const active = () => credentials.activeAll().pipe(Effect.orDie)
|
const active = Effect.fn("CatalogV2.active")(function* () {
|
||||||
|
return new Map((yield* credentials.all()).map((credential) => [credential.integrationID, credential]))
|
||||||
|
})
|
||||||
|
|
||||||
yield* events.subscribe(PluginV2.Event.Added).pipe(
|
yield* events.subscribe(PluginV2.Event.Added).pipe(
|
||||||
// Plugin registries are location scoped even though the event bus is process scoped.
|
// Plugin registries are location scoped even though the event bus is process scoped.
|
||||||
|
|||||||
@@ -1,492 +0,0 @@
|
|||||||
export * as Connector from "./connector"
|
|
||||||
|
|
||||||
import { Cause, Clock, Context, Duration, Effect, Exit, Layer, Schedule, Schema, Scope, SynchronizedRef } from "effect"
|
|
||||||
import { castDraft, enableMapSet, type Draft } from "immer"
|
|
||||||
import { Credential } from "./credential"
|
|
||||||
import { ConnectorSchema } from "./connector/schema"
|
|
||||||
import { withStatics } from "./schema"
|
|
||||||
import { State } from "./state"
|
|
||||||
import { Identifier } from "./util/identifier"
|
|
||||||
import { KeyedMutex } from "./effect/keyed-mutex"
|
|
||||||
import { EventV2 } from "./event"
|
|
||||||
|
|
||||||
export const ID = ConnectorSchema.ID
|
|
||||||
export type ID = ConnectorSchema.ID
|
|
||||||
|
|
||||||
export const MethodID = ConnectorSchema.MethodID
|
|
||||||
export type MethodID = ConnectorSchema.MethodID
|
|
||||||
|
|
||||||
export const AttemptID = Schema.String.pipe(
|
|
||||||
Schema.brand("Connector.AttemptID"),
|
|
||||||
withStatics((schema) => ({ create: () => schema.make("con_" + Identifier.ascending()) })),
|
|
||||||
)
|
|
||||||
export type AttemptID = typeof AttemptID.Type
|
|
||||||
|
|
||||||
export const When = Schema.Struct({
|
|
||||||
key: Schema.String,
|
|
||||||
op: Schema.Literals(["eq", "neq"]),
|
|
||||||
value: Schema.String,
|
|
||||||
}).annotate({ identifier: "Connector.When" })
|
|
||||||
export type When = typeof When.Type
|
|
||||||
|
|
||||||
export class TextPrompt extends Schema.Class<TextPrompt>("Connector.TextPrompt")({
|
|
||||||
type: Schema.Literal("text"),
|
|
||||||
key: Schema.String,
|
|
||||||
message: Schema.String,
|
|
||||||
placeholder: Schema.optional(Schema.String),
|
|
||||||
when: Schema.optional(When),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class SelectPrompt extends Schema.Class<SelectPrompt>("Connector.SelectPrompt")({
|
|
||||||
type: Schema.Literal("select"),
|
|
||||||
key: Schema.String,
|
|
||||||
message: Schema.String,
|
|
||||||
options: Schema.Array(
|
|
||||||
Schema.Struct({
|
|
||||||
label: Schema.String,
|
|
||||||
value: Schema.String,
|
|
||||||
hint: Schema.optional(Schema.String),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
when: Schema.optional(When),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export const Prompt = Schema.Union([TextPrompt, SelectPrompt]).pipe(Schema.toTaggedUnion("type"))
|
|
||||||
export type Prompt = typeof Prompt.Type
|
|
||||||
|
|
||||||
export class OAuthMethod extends Schema.Class<OAuthMethod>("Connector.OAuthMethod")({
|
|
||||||
id: MethodID,
|
|
||||||
type: Schema.Literal("oauth"),
|
|
||||||
label: Schema.String,
|
|
||||||
prompts: Schema.optional(Schema.Array(Prompt)),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class KeyMethod extends Schema.Class<KeyMethod>("Connector.KeyMethod")({
|
|
||||||
id: MethodID,
|
|
||||||
type: Schema.Literal("key"),
|
|
||||||
label: Schema.String,
|
|
||||||
prompts: Schema.optional(Schema.Array(Prompt)),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export const Method = Schema.Union([OAuthMethod, KeyMethod]).pipe(Schema.toTaggedUnion("type"))
|
|
||||||
export type Method = typeof Method.Type
|
|
||||||
|
|
||||||
export class Info extends Schema.Class<Info>("Connector.Info")({
|
|
||||||
id: ID,
|
|
||||||
name: Schema.String,
|
|
||||||
methods: Schema.Array(Method),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export type Inputs = Readonly<{ [key: string]: string }>
|
|
||||||
|
|
||||||
export type OAuthAuthorization = {
|
|
||||||
readonly url: string
|
|
||||||
readonly instructions: string
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
readonly mode: "auto"
|
|
||||||
readonly callback: Effect.Effect<Credential.Value, unknown>
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
readonly mode: "code"
|
|
||||||
readonly callback: (code: string) => Effect.Effect<Credential.Value, unknown>
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export interface OAuthImplementation {
|
|
||||||
readonly connectorID: ID
|
|
||||||
readonly method: OAuthMethod
|
|
||||||
readonly authorize: (inputs: Inputs) => Effect.Effect<OAuthAuthorization, unknown, Scope.Scope>
|
|
||||||
readonly refresh?: (credential: Credential.OAuth) => Effect.Effect<Credential.OAuth, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface KeyImplementation {
|
|
||||||
readonly connectorID: ID
|
|
||||||
readonly method: KeyMethod
|
|
||||||
readonly authorize: (key: string, inputs: Inputs) => Effect.Effect<Credential.Key, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Implementation = OAuthImplementation | KeyImplementation
|
|
||||||
|
|
||||||
function isKeyImplementation(implementation: Implementation): implementation is KeyImplementation {
|
|
||||||
return implementation.method.type === "key"
|
|
||||||
}
|
|
||||||
|
|
||||||
function isOAuthImplementation(implementation: Implementation): implementation is OAuthImplementation {
|
|
||||||
return implementation.method.type === "oauth"
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Attempt extends Schema.Class<Attempt>("Connector.Attempt")({
|
|
||||||
attemptID: AttemptID,
|
|
||||||
url: Schema.String,
|
|
||||||
instructions: Schema.String,
|
|
||||||
mode: Schema.Literals(["auto", "code"]),
|
|
||||||
time: Schema.Struct({
|
|
||||||
created: Schema.Number,
|
|
||||||
expires: Schema.Number,
|
|
||||||
}),
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
const Time = Schema.Struct({
|
|
||||||
created: Schema.Number,
|
|
||||||
expires: Schema.Number,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const AttemptStatus = Schema.Union([
|
|
||||||
Schema.Struct({ status: Schema.Literal("pending"), time: Time }),
|
|
||||||
Schema.Struct({ status: Schema.Literal("complete"), time: Time }),
|
|
||||||
Schema.Struct({ status: Schema.Literal("failed"), message: Schema.String, time: Time }),
|
|
||||||
Schema.Struct({ status: Schema.Literal("expired"), time: Time }),
|
|
||||||
]).pipe(Schema.toTaggedUnion("status"))
|
|
||||||
export type AttemptStatus = typeof AttemptStatus.Type
|
|
||||||
|
|
||||||
export class CodeRequiredError extends Schema.TaggedErrorClass<CodeRequiredError>()("Connector.CodeRequired", {
|
|
||||||
attemptID: AttemptID,
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationError>()("Connector.Authorization", {
|
|
||||||
cause: Schema.Defect,
|
|
||||||
}) {}
|
|
||||||
|
|
||||||
export type Error = CodeRequiredError | AuthorizationError
|
|
||||||
|
|
||||||
export const Event = {
|
|
||||||
Updated: EventV2.define({
|
|
||||||
type: "connector.updated",
|
|
||||||
schema: {},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
type Entry = {
|
|
||||||
connector: Info
|
|
||||||
implementations: Map<MethodID, Implementation>
|
|
||||||
}
|
|
||||||
|
|
||||||
type Data = {
|
|
||||||
connectors: Map<ID, Entry>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Editor = {
|
|
||||||
list: () => readonly Info[]
|
|
||||||
get: (id: ID) => Info | undefined
|
|
||||||
update: (id: ID, update: (connector: Draft<Omit<Info, "methods">>) => void) => void
|
|
||||||
remove: (id: ID) => void
|
|
||||||
method: {
|
|
||||||
update: (implementation: Implementation) => void
|
|
||||||
remove: (connectorID: ID, methodID: MethodID) => void
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Interface {
|
|
||||||
/** Registers a scoped transform over the connector registry. */
|
|
||||||
readonly transform: State.Interface<Data, Editor>["transform"]
|
|
||||||
/** Registers and immediately applies a scoped connector registry update. */
|
|
||||||
readonly update: State.Interface<Data, Editor>["update"]
|
|
||||||
/** Returns one connector with its serializable login methods. */
|
|
||||||
readonly get: (id: ID) => Effect.Effect<Info | undefined>
|
|
||||||
/** Returns all connectors with their serializable login methods. */
|
|
||||||
readonly list: () => Effect.Effect<Info[]>
|
|
||||||
/** Refreshes an OAuth credential with its originating method. */
|
|
||||||
readonly refresh: (credentialID: Credential.ID) => Effect.Effect<void, AuthorizationError>
|
|
||||||
readonly connect: {
|
|
||||||
/** Runs a key method and stores the resulting credential. */
|
|
||||||
readonly key: (input: {
|
|
||||||
/** Connector receiving the credential. */
|
|
||||||
readonly connectorID: ID
|
|
||||||
/** Key method selected by the caller. */
|
|
||||||
readonly methodID: MethodID
|
|
||||||
/** Secret entered by the user. */
|
|
||||||
readonly key: string
|
|
||||||
/** Answers to the method's optional prompts. */
|
|
||||||
readonly inputs: Inputs
|
|
||||||
/** User-facing label for the stored credential. */
|
|
||||||
readonly label?: string
|
|
||||||
}) => Effect.Effect<void, AuthorizationError>
|
|
||||||
readonly oauth: {
|
|
||||||
/** Starts a stateful OAuth attempt. */
|
|
||||||
readonly begin: (input: {
|
|
||||||
/** Connector being authenticated. */
|
|
||||||
readonly connectorID: ID
|
|
||||||
/** OAuth method selected by the caller. */
|
|
||||||
readonly methodID: MethodID
|
|
||||||
/** Answers to the method's optional prompts. */
|
|
||||||
readonly inputs: Inputs
|
|
||||||
/** User-facing label for the credential created on completion. */
|
|
||||||
readonly label?: string
|
|
||||||
}) => Effect.Effect<Attempt, AuthorizationError>
|
|
||||||
/** Returns the current state of an OAuth attempt. */
|
|
||||||
readonly status: (attemptID: AttemptID) => Effect.Effect<AttemptStatus>
|
|
||||||
/** Completes the attempt and stores its credential. */
|
|
||||||
readonly complete: (input: {
|
|
||||||
/** Opaque handle returned by `begin`. */
|
|
||||||
readonly attemptID: AttemptID
|
|
||||||
/** Authorization code required by attempts in code mode. */
|
|
||||||
readonly code?: string
|
|
||||||
}) => Effect.Effect<void, CodeRequiredError | AuthorizationError>
|
|
||||||
/** Cancels an attempt and releases its resources. */
|
|
||||||
readonly cancel: (attemptID: AttemptID) => Effect.Effect<void>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Connector") {}
|
|
||||||
|
|
||||||
enableMapSet()
|
|
||||||
|
|
||||||
const attemptLifetime = Duration.toMillis(Duration.minutes(10))
|
|
||||||
const terminalRetention = Duration.toMillis(Duration.minutes(1))
|
|
||||||
const scrubInterval = Duration.seconds(30)
|
|
||||||
|
|
||||||
type AttemptTime = { created: number; expires: number }
|
|
||||||
type PendingAttempt = {
|
|
||||||
status: "pending"
|
|
||||||
completing: boolean
|
|
||||||
authorization: OAuthAuthorization
|
|
||||||
connectorID: ID
|
|
||||||
methodID: MethodID
|
|
||||||
label?: string
|
|
||||||
scope: Scope.Closeable
|
|
||||||
time: AttemptTime
|
|
||||||
}
|
|
||||||
type TerminalAttempt = {
|
|
||||||
status: "complete" | "failed" | "expired"
|
|
||||||
message?: string
|
|
||||||
removeAt: number
|
|
||||||
time: AttemptTime
|
|
||||||
}
|
|
||||||
type AttemptEntry = PendingAttempt | TerminalAttempt
|
|
||||||
|
|
||||||
export const locationLayer = Layer.effect(
|
|
||||||
Service,
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const credentials = yield* Credential.Service
|
|
||||||
const events = yield* EventV2.Service
|
|
||||||
const scope = yield* Scope.Scope
|
|
||||||
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
|
|
||||||
const refreshLocks = KeyedMutex.makeUnsafe<Credential.ID>()
|
|
||||||
const state = State.create<Data, Editor>({
|
|
||||||
initial: () => ({ connectors: new Map<ID, Entry>() }),
|
|
||||||
editor: (draft) => ({
|
|
||||||
list: () => Array.from(draft.connectors.values(), (entry) => entry.connector) as Info[],
|
|
||||||
get: (id) => draft.connectors.get(id)?.connector as Info | undefined,
|
|
||||||
update: (id, update) => {
|
|
||||||
const current =
|
|
||||||
draft.connectors.get(id) ??
|
|
||||||
castDraft({ connector: new Info({ id, name: id, methods: [] }), implementations: new Map() })
|
|
||||||
if (!draft.connectors.has(id)) draft.connectors.set(id, current)
|
|
||||||
update(current.connector)
|
|
||||||
current.connector.id = id
|
|
||||||
},
|
|
||||||
remove: (id) => draft.connectors.delete(id),
|
|
||||||
method: {
|
|
||||||
update: (implementation) => {
|
|
||||||
const current =
|
|
||||||
draft.connectors.get(implementation.connectorID) ??
|
|
||||||
castDraft({
|
|
||||||
connector: new Info({ id: implementation.connectorID, name: implementation.connectorID, methods: [] }),
|
|
||||||
implementations: new Map<MethodID, Implementation>(),
|
|
||||||
})
|
|
||||||
if (!draft.connectors.has(implementation.connectorID)) {
|
|
||||||
draft.connectors.set(implementation.connectorID, current)
|
|
||||||
}
|
|
||||||
const index = current.connector.methods.findIndex((method) => method.id === implementation.method.id)
|
|
||||||
if (index === -1) current.connector.methods.push(castDraft(implementation.method))
|
|
||||||
else current.connector.methods[index] = castDraft(implementation.method)
|
|
||||||
current.implementations.set(implementation.method.id, castDraft(implementation))
|
|
||||||
},
|
|
||||||
remove: (connectorID, methodID) => {
|
|
||||||
const current = draft.connectors.get(connectorID)
|
|
||||||
if (!current) return
|
|
||||||
const index = current.connector.methods.findIndex((method) => method.id === methodID)
|
|
||||||
if (index !== -1) current.connector.methods.splice(index, 1)
|
|
||||||
current.implementations.delete(methodID)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
|
|
||||||
})
|
|
||||||
|
|
||||||
const authorize = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
||||||
effect.pipe(Effect.mapError((cause) => new AuthorizationError({ cause })))
|
|
||||||
|
|
||||||
const close = (attemptScope: Scope.Closeable) =>
|
|
||||||
Scope.close(attemptScope, Exit.void).pipe(Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid)
|
|
||||||
|
|
||||||
const message = (cause: Cause.Cause<unknown>) => {
|
|
||||||
const error = Cause.squash(cause)
|
|
||||||
return error instanceof Error ? error.message : String(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
const settle = Effect.fnUntraced(function* (attemptID: AttemptID, exit: Exit.Exit<Credential.Value, unknown>) {
|
|
||||||
const now = yield* Clock.currentTimeMillis
|
|
||||||
const result = yield* SynchronizedRef.modify(attempts, (current) => {
|
|
||||||
const attempt = current.get(attemptID)
|
|
||||||
if (!attempt || attempt.status !== "pending") return [undefined, current]
|
|
||||||
const terminal: TerminalAttempt = Exit.isSuccess(exit)
|
|
||||||
? { status: "complete", time: attempt.time, removeAt: now + terminalRetention }
|
|
||||||
: { status: "failed", message: message(exit.cause), time: attempt.time, removeAt: now + terminalRetention }
|
|
||||||
return [attempt, new Map(current).set(attemptID, terminal)]
|
|
||||||
})
|
|
||||||
if (!result) return
|
|
||||||
if (Exit.isSuccess(exit)) {
|
|
||||||
yield* credentials.create({
|
|
||||||
connectorID: result.connectorID,
|
|
||||||
methodID: result.methodID,
|
|
||||||
label: result.label,
|
|
||||||
value: exit.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
yield* close(result.scope)
|
|
||||||
})
|
|
||||||
|
|
||||||
const scrub = Effect.fnUntraced(function* () {
|
|
||||||
const now = yield* Clock.currentTimeMillis
|
|
||||||
const expired = yield* SynchronizedRef.modify(attempts, (current) => {
|
|
||||||
const next = new Map(current)
|
|
||||||
const scopes: Scope.Closeable[] = []
|
|
||||||
for (const [id, attempt] of current) {
|
|
||||||
if (attempt.status === "pending" && attempt.time.expires <= now) {
|
|
||||||
scopes.push(attempt.scope)
|
|
||||||
next.set(id, { status: "expired", time: attempt.time, removeAt: now + terminalRetention })
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (attempt.status !== "pending" && attempt.removeAt <= now) next.delete(id)
|
|
||||||
}
|
|
||||||
return [scopes, next]
|
|
||||||
})
|
|
||||||
yield* Effect.forEach(expired, close, { discard: true })
|
|
||||||
})
|
|
||||||
|
|
||||||
yield* scrub().pipe(Effect.repeat(Schedule.spaced(scrubInterval)), Effect.forkIn(scope))
|
|
||||||
|
|
||||||
return Service.of({
|
|
||||||
transform: state.transform,
|
|
||||||
update: state.update,
|
|
||||||
get: Effect.fn("Connector.get")(function* (id) {
|
|
||||||
return state.get().connectors.get(id)?.connector
|
|
||||||
}),
|
|
||||||
list: Effect.fn("Connector.list")(function* () {
|
|
||||||
return Array.from(state.get().connectors.values(), (record) => record.connector).toSorted((a, b) =>
|
|
||||||
a.name.localeCompare(b.name),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
refresh: Effect.fn("Connector.refresh")(function* (credentialID) {
|
|
||||||
yield* refreshLocks.withLock(credentialID)(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const credential = yield* credentials.get(credentialID)
|
|
||||||
if (!credential || credential.value.type !== "oauth") {
|
|
||||||
return yield* Effect.die(`OAuth credential not found: ${credentialID}`)
|
|
||||||
}
|
|
||||||
const implementation = state
|
|
||||||
.get()
|
|
||||||
.connectors.get(credential.connectorID)
|
|
||||||
?.implementations.get(credential.methodID)
|
|
||||||
if (!implementation || !isOAuthImplementation(implementation) || !implementation.refresh) {
|
|
||||||
return yield* Effect.die(
|
|
||||||
`OAuth refresh method not found: ${credential.connectorID}/${credential.methodID}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const value = yield* authorize(implementation.refresh(credential.value))
|
|
||||||
yield* credentials.update(credential.id, { value })
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
connect: {
|
|
||||||
key: Effect.fn("Connector.connect.key")(function* (input) {
|
|
||||||
const method = state.get().connectors.get(input.connectorID)?.implementations.get(input.methodID)
|
|
||||||
if (!method || !isKeyImplementation(method)) {
|
|
||||||
return yield* Effect.die(`Key method not found: ${input.connectorID}/${input.methodID}`)
|
|
||||||
}
|
|
||||||
const value = yield* authorize(method.authorize(input.key, input.inputs))
|
|
||||||
yield* credentials.create({
|
|
||||||
connectorID: input.connectorID,
|
|
||||||
methodID: input.methodID,
|
|
||||||
label: input.label,
|
|
||||||
value,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
oauth: {
|
|
||||||
begin: Effect.fn("Connector.connect.oauth.begin")(function* (input) {
|
|
||||||
const method = state.get().connectors.get(input.connectorID)?.implementations.get(input.methodID)
|
|
||||||
if (!method || !isOAuthImplementation(method)) {
|
|
||||||
return yield* Effect.die(`OAuth method not found: ${input.connectorID}/${input.methodID}`)
|
|
||||||
}
|
|
||||||
const attemptScope = yield* Scope.fork(scope)
|
|
||||||
const authorization = yield* authorize(method.authorize(input.inputs)).pipe(
|
|
||||||
Scope.provide(attemptScope),
|
|
||||||
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void)),
|
|
||||||
)
|
|
||||||
const id = AttemptID.create()
|
|
||||||
const created = yield* Clock.currentTimeMillis
|
|
||||||
const time = { created, expires: created + attemptLifetime }
|
|
||||||
yield* SynchronizedRef.update(attempts, (current) =>
|
|
||||||
new Map(current).set(id, {
|
|
||||||
status: "pending",
|
|
||||||
completing: authorization.mode === "auto",
|
|
||||||
authorization,
|
|
||||||
connectorID: input.connectorID,
|
|
||||||
methodID: input.methodID,
|
|
||||||
label: input.label,
|
|
||||||
scope: attemptScope,
|
|
||||||
time,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
if (authorization.mode === "auto") {
|
|
||||||
yield* authorization.callback.pipe(
|
|
||||||
Effect.exit,
|
|
||||||
Effect.flatMap((exit) => settle(id, exit)),
|
|
||||||
Effect.forkIn(attemptScope, { startImmediately: true }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return new Attempt({
|
|
||||||
attemptID: id,
|
|
||||||
url: authorization.url,
|
|
||||||
instructions: authorization.instructions,
|
|
||||||
mode: authorization.mode,
|
|
||||||
time,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
status: Effect.fn("Connector.connect.oauth.status")(function* (attemptID) {
|
|
||||||
const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID)
|
|
||||||
if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${attemptID}`)
|
|
||||||
if (attempt.status === "failed") {
|
|
||||||
return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time }
|
|
||||||
}
|
|
||||||
return { status: attempt.status, time: attempt.time }
|
|
||||||
}),
|
|
||||||
complete: Effect.fn("Connector.connect.oauth.complete")(function* (input) {
|
|
||||||
const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
|
|
||||||
const match = current.get(input.attemptID)
|
|
||||||
if (!match || match.status !== "pending" || match.completing) return [match, current]
|
|
||||||
if (match.authorization.mode === "code" && input.code === undefined) return [match, current]
|
|
||||||
return [match, new Map(current).set(input.attemptID, { ...match, completing: true })]
|
|
||||||
})
|
|
||||||
if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${input.attemptID}`)
|
|
||||||
if (attempt.status !== "pending") return
|
|
||||||
if (attempt.authorization.mode === "code" && input.code === undefined) {
|
|
||||||
return yield* new CodeRequiredError({ attemptID: input.attemptID })
|
|
||||||
}
|
|
||||||
if (attempt.completing) return yield* Effect.die(`OAuth attempt already completing: ${input.attemptID}`)
|
|
||||||
const callback =
|
|
||||||
attempt.authorization.mode === "auto"
|
|
||||||
? attempt.authorization.callback
|
|
||||||
: attempt.authorization.callback(input.code as string)
|
|
||||||
const exit = yield* authorize(callback).pipe(Effect.exit)
|
|
||||||
yield* settle(input.attemptID, exit)
|
|
||||||
if (Exit.isFailure(exit)) return yield* exit
|
|
||||||
}),
|
|
||||||
cancel: Effect.fn("Connector.connect.oauth.cancel")(function* (attemptID) {
|
|
||||||
const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
|
|
||||||
const match = current.get(attemptID)
|
|
||||||
if (!match || match.status !== "pending") return [undefined, current]
|
|
||||||
const next = new Map(current)
|
|
||||||
next.delete(attemptID)
|
|
||||||
return [match, next]
|
|
||||||
})
|
|
||||||
if (attempt) yield* Scope.close(attempt.scope, Exit.void)
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
export * as ConnectorSchema from "./schema"
|
|
||||||
|
|
||||||
import { Schema } from "effect"
|
|
||||||
|
|
||||||
export const ID = Schema.String.pipe(Schema.brand("Connector.ID"))
|
|
||||||
export type ID = typeof ID.Type
|
|
||||||
|
|
||||||
export const MethodID = Schema.String.pipe(Schema.brand("Connector.MethodID"))
|
|
||||||
export type MethodID = typeof MethodID.Type
|
|
||||||
+40
-245
@@ -1,17 +1,12 @@
|
|||||||
export * as Credential from "./credential"
|
export * as Credential from "./credential"
|
||||||
|
|
||||||
import { and, asc, eq, ne } from "drizzle-orm"
|
import { asc, eq } from "drizzle-orm"
|
||||||
import { Context, Effect, Layer, Option, Schema } from "effect"
|
import { Context, Effect, Layer, Schema } from "effect"
|
||||||
import { Database } from "./database/database"
|
import { Database } from "./database/database"
|
||||||
import { ConnectorSchema } from "./connector/schema"
|
import { IntegrationSchema } from "./integration/schema"
|
||||||
import { EventV2 } from "./event"
|
|
||||||
import { NonNegativeInt, withStatics } from "./schema"
|
import { NonNegativeInt, withStatics } from "./schema"
|
||||||
import { CredentialTable } from "./credential/sql"
|
|
||||||
import { Identifier } from "./util/identifier"
|
import { Identifier } from "./util/identifier"
|
||||||
import { FSUtil } from "./fs-util"
|
import { CredentialTable } from "./credential/sql"
|
||||||
import { Global } from "./global"
|
|
||||||
import { DataMigrationTable } from "./data-migration.sql"
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
export const ID = Schema.String.pipe(
|
export const ID = Schema.String.pipe(
|
||||||
Schema.brand("Credential.ID"),
|
Schema.brand("Credential.ID"),
|
||||||
@@ -21,6 +16,7 @@ export type ID = typeof ID.Type
|
|||||||
|
|
||||||
export class OAuth extends Schema.Class<OAuth>("Credential.OAuth")({
|
export class OAuth extends Schema.Class<OAuth>("Credential.OAuth")({
|
||||||
type: Schema.Literal("oauth"),
|
type: Schema.Literal("oauth"),
|
||||||
|
methodID: IntegrationSchema.MethodID,
|
||||||
refresh: Schema.String,
|
refresh: Schema.String,
|
||||||
access: Schema.String,
|
access: Schema.String,
|
||||||
expires: NonNegativeInt,
|
expires: NonNegativeInt,
|
||||||
@@ -33,255 +29,94 @@ export class Key extends Schema.Class<Key>("Credential.Key")({
|
|||||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export const Value = Schema.Union([OAuth, Key])
|
export const Info = Schema.Union([OAuth, Key])
|
||||||
.pipe(Schema.toTaggedUnion("type"))
|
.pipe(Schema.toTaggedUnion("type"))
|
||||||
.annotate({ identifier: "Credential.Value" })
|
.annotate({ identifier: "Credential.Info" })
|
||||||
export type Value = Schema.Schema.Type<typeof Value>
|
export type Info = Schema.Schema.Type<typeof Info>
|
||||||
|
|
||||||
const LegacyOAuth = Schema.Struct({
|
export class Stored extends Schema.Class<Stored>("Credential.Stored")({
|
||||||
type: Schema.Literal("oauth"),
|
|
||||||
refresh: Schema.String,
|
|
||||||
access: Schema.String,
|
|
||||||
expires: NonNegativeInt,
|
|
||||||
accountId: Schema.optional(Schema.String),
|
|
||||||
enterpriseUrl: Schema.optional(Schema.String),
|
|
||||||
})
|
|
||||||
|
|
||||||
const LegacyKey = Schema.Struct({
|
|
||||||
type: Schema.Literal("api"),
|
|
||||||
key: Schema.String,
|
|
||||||
metadata: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
||||||
})
|
|
||||||
|
|
||||||
const LegacyValue = Schema.Union([LegacyOAuth, LegacyKey])
|
|
||||||
|
|
||||||
export class Info extends Schema.Class<Info>("Credential.Info")({
|
|
||||||
id: ID,
|
id: ID,
|
||||||
connectorID: ConnectorSchema.ID,
|
integrationID: IntegrationSchema.ID,
|
||||||
methodID: ConnectorSchema.MethodID,
|
|
||||||
label: Schema.String,
|
label: Schema.String,
|
||||||
value: Value,
|
value: Info,
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
export const Event = {
|
|
||||||
Added: EventV2.define({
|
|
||||||
type: "credential.added",
|
|
||||||
schema: { credential: Info },
|
|
||||||
}),
|
|
||||||
Removed: EventV2.define({
|
|
||||||
type: "credential.removed",
|
|
||||||
schema: { credential: Info },
|
|
||||||
}),
|
|
||||||
Switched: EventV2.define({
|
|
||||||
type: "credential.switched",
|
|
||||||
schema: {
|
|
||||||
connectorID: ConnectorSchema.ID,
|
|
||||||
from: Schema.optional(ID),
|
|
||||||
to: Schema.optional(ID),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Interface {
|
export interface Interface {
|
||||||
readonly get: (id: ID) => Effect.Effect<Info | undefined>
|
/** Returns every stored credential. */
|
||||||
readonly all: () => Effect.Effect<Info[]>
|
readonly all: () => Effect.Effect<Stored[]>
|
||||||
|
/** Returns stored credentials belonging to one integration. */
|
||||||
|
readonly list: (integrationID: IntegrationSchema.ID) => Effect.Effect<Stored[]>
|
||||||
|
/** Replaces any credential for an integration and returns the new record. */
|
||||||
readonly create: (input: {
|
readonly create: (input: {
|
||||||
connectorID: ConnectorSchema.ID
|
readonly integrationID: IntegrationSchema.ID
|
||||||
methodID: ConnectorSchema.MethodID
|
readonly value: Info
|
||||||
value: Value
|
readonly label?: string
|
||||||
label?: string
|
}) => Effect.Effect<Stored>
|
||||||
}) => Effect.Effect<Info>
|
/** Updates the label or secret value of a stored credential. */
|
||||||
readonly update: (id: ID, updates: Partial<Pick<Info, "label" | "value">>) => Effect.Effect<void>
|
readonly update: (id: ID, updates: Partial<Pick<Stored, "label" | "value">>) => Effect.Effect<void>
|
||||||
|
/** Removes a stored credential. */
|
||||||
readonly remove: (id: ID) => Effect.Effect<void>
|
readonly remove: (id: ID) => Effect.Effect<void>
|
||||||
readonly activate: (id: ID) => Effect.Effect<void>
|
|
||||||
readonly active: (connectorID: ConnectorSchema.ID) => Effect.Effect<Info | undefined>
|
|
||||||
readonly activeAll: () => Effect.Effect<Map<ConnectorSchema.ID, Info>>
|
|
||||||
readonly forConnector: (connectorID: ConnectorSchema.ID) => Effect.Effect<Info[]>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Credential") {}
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Credential") {}
|
||||||
|
|
||||||
export const legacyImportLayer = Layer.effectDiscard(
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const { db } = yield* Database.Service
|
|
||||||
const fs = yield* FSUtil.Service
|
|
||||||
const global = yield* Global.Service
|
|
||||||
const name = "credential.auth-json"
|
|
||||||
if (yield* db.select().from(DataMigrationTable).where(eq(DataMigrationTable.name, name)).get()) return
|
|
||||||
const raw = yield* fs.readJson(path.join(global.data, "auth.json")).pipe(Effect.option)
|
|
||||||
if (Option.isNone(raw) || typeof raw.value !== "object" || raw.value === null || Array.isArray(raw.value)) return
|
|
||||||
const decode = Schema.decodeUnknownOption(LegacyValue)
|
|
||||||
const values = Object.entries(raw.value).flatMap(([connectorID, value]) => {
|
|
||||||
const decoded = decode(value)
|
|
||||||
if (Option.isNone(decoded)) return []
|
|
||||||
const credential = decoded.value
|
|
||||||
const id = ID.create()
|
|
||||||
const connector = ConnectorSchema.ID.make(connectorID.replace(/\/+$/, ""))
|
|
||||||
const methodID = ConnectorSchema.MethodID.make(
|
|
||||||
credential.type === "api"
|
|
||||||
? "api-key"
|
|
||||||
: connector === ConnectorSchema.ID.make("openai")
|
|
||||||
? "chatgpt-browser"
|
|
||||||
: "oauth",
|
|
||||||
)
|
|
||||||
const next: Value =
|
|
||||||
credential.type === "api"
|
|
||||||
? new Key({ type: "key", key: credential.key, metadata: credential.metadata })
|
|
||||||
: new OAuth({
|
|
||||||
type: "oauth",
|
|
||||||
refresh: credential.refresh,
|
|
||||||
access: credential.access,
|
|
||||||
expires: credential.expires,
|
|
||||||
metadata: {
|
|
||||||
...(credential.accountId ? { accountID: credential.accountId } : {}),
|
|
||||||
...(credential.enterpriseUrl ? { enterpriseURL: credential.enterpriseUrl } : {}),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return [{ id, connectorID: connector, methodID, value: next }]
|
|
||||||
})
|
|
||||||
yield* db.transaction((tx) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
for (const item of values) {
|
|
||||||
if (
|
|
||||||
yield* tx
|
|
||||||
.select({ id: CredentialTable.id })
|
|
||||||
.from(CredentialTable)
|
|
||||||
.where(eq(CredentialTable.connector_id, item.connectorID))
|
|
||||||
.get()
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
yield* tx.insert(CredentialTable).values({
|
|
||||||
id: item.id,
|
|
||||||
connector_id: item.connectorID,
|
|
||||||
method_id: item.methodID,
|
|
||||||
label: "Imported",
|
|
||||||
value: item.value,
|
|
||||||
active: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
yield* tx.insert(DataMigrationTable).values({ name, time_completed: Date.now() }).onConflictDoNothing().run()
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}).pipe(Effect.orDie),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const layer = Layer.effect(
|
export const layer = Layer.effect(
|
||||||
Service,
|
Service,
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const { db } = yield* Database.Service
|
const { db } = yield* Database.Service
|
||||||
const events = yield* EventV2.Service
|
const decode = Schema.decodeUnknownSync(Info)
|
||||||
const decodeValue = Schema.decodeUnknownSync(Value)
|
const stored = (row: typeof CredentialTable.$inferSelect) =>
|
||||||
const info = (row: typeof CredentialTable.$inferSelect) =>
|
new Stored({
|
||||||
new Info({
|
|
||||||
id: row.id,
|
id: row.id,
|
||||||
connectorID: row.connector_id,
|
integrationID: row.integration_id,
|
||||||
methodID: row.method_id,
|
|
||||||
label: row.label,
|
label: row.label,
|
||||||
value: decodeValue(row.value),
|
value: decode(row.value),
|
||||||
})
|
})
|
||||||
|
|
||||||
const activate = Effect.fn("Credential.activate")(function* (id: ID) {
|
|
||||||
const switched = yield* db
|
|
||||||
.transaction((tx) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const credential = yield* tx.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get()
|
|
||||||
if (!credential || credential.active) return
|
|
||||||
const current = yield* tx
|
|
||||||
.select({ id: CredentialTable.id })
|
|
||||||
.from(CredentialTable)
|
|
||||||
.where(and(eq(CredentialTable.connector_id, credential.connector_id), eq(CredentialTable.active, true)))
|
|
||||||
.get()
|
|
||||||
yield* tx
|
|
||||||
.update(CredentialTable)
|
|
||||||
.set({ active: false })
|
|
||||||
.where(eq(CredentialTable.connector_id, credential.connector_id))
|
|
||||||
.run()
|
|
||||||
yield* tx.update(CredentialTable).set({ active: true }).where(eq(CredentialTable.id, id)).run()
|
|
||||||
return { connectorID: credential.connector_id, from: current?.id, to: id }
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
if (switched) yield* events.publish(Event.Switched, switched)
|
|
||||||
})
|
|
||||||
|
|
||||||
return Service.of({
|
return Service.of({
|
||||||
get: Effect.fn("Credential.get")(function* (id) {
|
|
||||||
const row = yield* db.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get().pipe(Effect.orDie)
|
|
||||||
return row ? info(row) : undefined
|
|
||||||
}),
|
|
||||||
all: Effect.fn("Credential.all")(function* () {
|
all: Effect.fn("Credential.all")(function* () {
|
||||||
return (yield* db
|
return (yield* db
|
||||||
.select()
|
.select()
|
||||||
.from(CredentialTable)
|
.from(CredentialTable)
|
||||||
.orderBy(asc(CredentialTable.time_created))
|
.orderBy(asc(CredentialTable.time_created))
|
||||||
.all()
|
.all()
|
||||||
.pipe(Effect.orDie)).map(info)
|
.pipe(Effect.orDie)).map(stored)
|
||||||
}),
|
}),
|
||||||
active: Effect.fn("Credential.active")(function* (connectorID) {
|
list: Effect.fn("Credential.list")(function* (integrationID) {
|
||||||
const row = yield* db
|
|
||||||
.select()
|
|
||||||
.from(CredentialTable)
|
|
||||||
.where(and(eq(CredentialTable.connector_id, connectorID), eq(CredentialTable.active, true)))
|
|
||||||
.get()
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
return row ? info(row) : undefined
|
|
||||||
}),
|
|
||||||
activeAll: Effect.fn("Credential.activeAll")(function* () {
|
|
||||||
const rows = yield* db
|
|
||||||
.select()
|
|
||||||
.from(CredentialTable)
|
|
||||||
.where(eq(CredentialTable.active, true))
|
|
||||||
.all()
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
return new Map(rows.map((row) => [row.connector_id, info(row)]))
|
|
||||||
}),
|
|
||||||
forConnector: Effect.fn("Credential.forConnector")(function* (connectorID) {
|
|
||||||
return (yield* db
|
return (yield* db
|
||||||
.select()
|
.select()
|
||||||
.from(CredentialTable)
|
.from(CredentialTable)
|
||||||
.where(eq(CredentialTable.connector_id, connectorID))
|
.where(eq(CredentialTable.integration_id, integrationID))
|
||||||
.orderBy(asc(CredentialTable.time_created))
|
.orderBy(asc(CredentialTable.time_created))
|
||||||
.all()
|
.all()
|
||||||
.pipe(Effect.orDie)).map(info)
|
.pipe(Effect.orDie)).map(stored)
|
||||||
}),
|
}),
|
||||||
create: Effect.fn("Credential.create")(function* (input) {
|
create: Effect.fn("Credential.create")(function* (input) {
|
||||||
const credential = new Info({
|
const credential = new Stored({
|
||||||
id: ID.create(),
|
id: ID.create(),
|
||||||
connectorID: input.connectorID,
|
integrationID: input.integrationID,
|
||||||
methodID: input.methodID,
|
|
||||||
label: input.label ?? "default",
|
label: input.label ?? "default",
|
||||||
value: input.value,
|
value: input.value,
|
||||||
})
|
})
|
||||||
const from = yield* db
|
yield* db
|
||||||
.transaction((tx) =>
|
.transaction((tx) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const current = yield* tx
|
|
||||||
.select({ id: CredentialTable.id })
|
|
||||||
.from(CredentialTable)
|
|
||||||
.where(and(eq(CredentialTable.connector_id, input.connectorID), eq(CredentialTable.active, true)))
|
|
||||||
.get()
|
|
||||||
yield* tx
|
yield* tx
|
||||||
.update(CredentialTable)
|
.delete(CredentialTable)
|
||||||
.set({ active: false })
|
.where(eq(CredentialTable.integration_id, credential.integrationID))
|
||||||
.where(eq(CredentialTable.connector_id, input.connectorID))
|
|
||||||
.run()
|
.run()
|
||||||
yield* tx
|
yield* tx
|
||||||
.insert(CredentialTable)
|
.insert(CredentialTable)
|
||||||
.values({
|
.values({
|
||||||
id: credential.id,
|
id: credential.id,
|
||||||
connector_id: credential.connectorID,
|
integration_id: credential.integrationID,
|
||||||
method_id: credential.methodID,
|
|
||||||
label: credential.label,
|
label: credential.label,
|
||||||
value: credential.value,
|
value: credential.value,
|
||||||
active: true,
|
|
||||||
})
|
})
|
||||||
.run()
|
.run()
|
||||||
return current?.id
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
yield* events.publish(Event.Added, { credential })
|
|
||||||
yield* events.publish(Event.Switched, { connectorID: credential.connectorID, from, to: credential.id })
|
|
||||||
return credential
|
return credential
|
||||||
}),
|
}),
|
||||||
update: Effect.fn("Credential.update")(function* (id, updates) {
|
update: Effect.fn("Credential.update")(function* (id, updates) {
|
||||||
@@ -294,50 +129,10 @@ export const layer = Layer.effect(
|
|||||||
.pipe(Effect.orDie)
|
.pipe(Effect.orDie)
|
||||||
}),
|
}),
|
||||||
remove: Effect.fn("Credential.remove")(function* (id) {
|
remove: Effect.fn("Credential.remove")(function* (id) {
|
||||||
const removed = yield* db
|
yield* db.delete(CredentialTable).where(eq(CredentialTable.id, id)).run().pipe(Effect.orDie)
|
||||||
.transaction((tx) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const row = yield* tx.select().from(CredentialTable).where(eq(CredentialTable.id, id)).get()
|
|
||||||
if (!row) return
|
|
||||||
yield* tx.delete(CredentialTable).where(eq(CredentialTable.id, id)).run()
|
|
||||||
if (!row.active) return { credential: info(row) }
|
|
||||||
const replacement = yield* tx
|
|
||||||
.select()
|
|
||||||
.from(CredentialTable)
|
|
||||||
.where(and(eq(CredentialTable.connector_id, row.connector_id), ne(CredentialTable.id, id)))
|
|
||||||
.orderBy(asc(CredentialTable.time_created))
|
|
||||||
.get()
|
|
||||||
if (replacement) {
|
|
||||||
yield* tx
|
|
||||||
.update(CredentialTable)
|
|
||||||
.set({ active: true })
|
|
||||||
.where(eq(CredentialTable.id, replacement.id))
|
|
||||||
.run()
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
credential: info(row),
|
|
||||||
switched: { connectorID: row.connector_id, from: id, to: replacement?.id },
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.pipe(Effect.orDie)
|
|
||||||
if (!removed) return
|
|
||||||
yield* events.publish(Event.Removed, { credential: removed.credential })
|
|
||||||
if (removed.switched) yield* events.publish(Event.Switched, removed.switched)
|
|
||||||
}),
|
}),
|
||||||
activate,
|
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const defaultLayer = layer.pipe(
|
export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))
|
||||||
Layer.provide(Database.defaultLayer),
|
|
||||||
Layer.provide(EventV2.defaultLayer),
|
|
||||||
Layer.provideMerge(
|
|
||||||
legacyImportLayer.pipe(
|
|
||||||
Layer.provide(Database.defaultLayer),
|
|
||||||
Layer.provide(FSUtil.defaultLayer),
|
|
||||||
Layer.provide(Global.defaultLayer),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,23 +1,12 @@
|
|||||||
import { sql } from "drizzle-orm"
|
import { sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||||
import { integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core"
|
|
||||||
import { Timestamps } from "../database/schema.sql"
|
import { Timestamps } from "../database/schema.sql"
|
||||||
import type { ConnectorSchema } from "../connector/schema"
|
import type { IntegrationSchema } from "../integration/schema"
|
||||||
import type { Credential } from "../credential"
|
import type { Credential } from "../credential"
|
||||||
|
|
||||||
export const CredentialTable = sqliteTable(
|
export const CredentialTable = sqliteTable("credential", {
|
||||||
"credential",
|
id: text().$type<Credential.ID>().primaryKey(),
|
||||||
{
|
integration_id: text().$type<IntegrationSchema.ID>().notNull(),
|
||||||
id: text().$type<Credential.ID>().primaryKey(),
|
label: text().notNull(),
|
||||||
connector_id: text().$type<ConnectorSchema.ID>().notNull(),
|
value: text({ mode: "json" }).$type<Credential.Info>().notNull(),
|
||||||
method_id: text().$type<ConnectorSchema.MethodID>().notNull(),
|
...Timestamps,
|
||||||
label: text().notNull(),
|
})
|
||||||
value: text({ mode: "json" }).$type<Credential.Value>().notNull(),
|
|
||||||
active: integer({ mode: "boolean" }).notNull().default(false),
|
|
||||||
...Timestamps,
|
|
||||||
},
|
|
||||||
(table) => [
|
|
||||||
uniqueIndex("credential_connector_active_idx")
|
|
||||||
.on(table.connector_id)
|
|
||||||
.where(sql`${table.active} = 1`),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|||||||
+1
@@ -35,5 +35,6 @@ export const migrations = (
|
|||||||
import("./migration/20260605003541_add_session_context_snapshot"),
|
import("./migration/20260605003541_add_session_context_snapshot"),
|
||||||
import("./migration/20260605042240_add_context_epoch_agent"),
|
import("./migration/20260605042240_add_context_epoch_agent"),
|
||||||
import("./migration/20260611035744_credential"),
|
import("./migration/20260611035744_credential"),
|
||||||
|
import("./migration/20260611192811_lush_chimera"),
|
||||||
])
|
])
|
||||||
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { sql } from "drizzle-orm"
|
|||||||
import { Effect, Semaphore } from "effect"
|
import { Effect, Semaphore } from "effect"
|
||||||
import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
import type { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite"
|
||||||
import { migrations } from "./migration.gen"
|
import { migrations } from "./migration.gen"
|
||||||
|
import schema from "./schema.gen"
|
||||||
|
|
||||||
type Database = EffectDrizzleSqlite.EffectSQLiteDatabase
|
type Database = EffectDrizzleSqlite.EffectSQLiteDatabase
|
||||||
type Transaction = Parameters<Parameters<Database["transaction"]>[0]>[0]
|
type Transaction = Parameters<Parameters<Database["transaction"]>[0]>[0]
|
||||||
@@ -15,7 +16,28 @@ export type Migration = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function apply(db: Database) {
|
export function apply(db: Database) {
|
||||||
return lock.withPermit(applyOnly(db, migrations))
|
return lock.withPermit(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const tables = yield* db.all<{ name: string }>(
|
||||||
|
sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`,
|
||||||
|
)
|
||||||
|
if (tables.some((table) => table.name === "session")) return yield* applyOnly(db, migrations)
|
||||||
|
if (tables.length > 0) return yield* Effect.die("Database is not empty and has no session table")
|
||||||
|
yield* db.transaction((tx) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* schema.up(tx)
|
||||||
|
yield* tx.run(
|
||||||
|
sql`CREATE TABLE ${sql.identifier("migration")} (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`,
|
||||||
|
)
|
||||||
|
yield* Effect.forEach(migrations, (migration) =>
|
||||||
|
tx.run(
|
||||||
|
sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyOnly(db: Database, input: Migration[]) {
|
export function applyOnly(db: Database, input: Migration[]) {
|
||||||
@@ -48,7 +70,7 @@ export function applyOnly(db: Database, input: Migration[]) {
|
|||||||
if (completed.has(migration.id)) continue
|
if (completed.has(migration.id)) continue
|
||||||
yield* db.transaction((tx) =>
|
yield* db.transaction((tx) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
if (!process.env.OPENCODE_SKIP_MIGRATIONS) yield* migration.up(tx)
|
yield* migration.up(tx)
|
||||||
yield* tx.run(
|
yield* tx.run(
|
||||||
sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`,
|
sql`INSERT INTO ${sql.identifier("migration")} (id, time_completed) VALUES (${migration.id}, ${Date.now()})`,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Effect } from "effect"
|
||||||
|
import type { DatabaseMigration } from "../migration"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
id: "20260611192811_lush_chimera",
|
||||||
|
up(tx) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* tx.run(`ALTER TABLE \`credential\` ADD \`integration_id\` text NOT NULL;`)
|
||||||
|
yield* tx.run(`DROP INDEX IF EXISTS \`credential_connector_active_idx\`;`)
|
||||||
|
yield* tx.run(`ALTER TABLE \`credential\` DROP COLUMN \`connector_id\`;`)
|
||||||
|
yield* tx.run(`ALTER TABLE \`credential\` DROP COLUMN \`method_id\`;`)
|
||||||
|
yield* tx.run(`ALTER TABLE \`credential\` DROP COLUMN \`active\`;`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} satisfies DatabaseMigration.Migration
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import { Effect } from "effect"
|
||||||
|
import type { DatabaseMigration } from "./migration"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
up(tx) {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`workspace\` (
|
||||||
|
\`id\` text PRIMARY KEY,
|
||||||
|
\`type\` text NOT NULL,
|
||||||
|
\`name\` text DEFAULT '' NOT NULL,
|
||||||
|
\`branch\` text,
|
||||||
|
\`directory\` text,
|
||||||
|
\`extra\` text,
|
||||||
|
\`project_id\` text NOT NULL,
|
||||||
|
\`time_used\` integer NOT NULL,
|
||||||
|
CONSTRAINT \`fk_workspace_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`data_migration\` (
|
||||||
|
\`name\` text PRIMARY KEY,
|
||||||
|
\`time_completed\` integer NOT NULL
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`account_state\` (
|
||||||
|
\`id\` integer PRIMARY KEY,
|
||||||
|
\`active_account_id\` text,
|
||||||
|
\`active_org_id\` text,
|
||||||
|
CONSTRAINT \`fk_account_state_active_account_id_account_id_fk\` FOREIGN KEY (\`active_account_id\`) REFERENCES \`account\`(\`id\`) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`account\` (
|
||||||
|
\`id\` text PRIMARY KEY,
|
||||||
|
\`email\` text NOT NULL,
|
||||||
|
\`url\` text NOT NULL,
|
||||||
|
\`access_token\` text NOT NULL,
|
||||||
|
\`refresh_token\` text NOT NULL,
|
||||||
|
\`token_expiry\` integer,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
\`time_updated\` integer NOT NULL
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`control_account\` (
|
||||||
|
\`email\` text NOT NULL,
|
||||||
|
\`url\` text NOT NULL,
|
||||||
|
\`access_token\` text NOT NULL,
|
||||||
|
\`refresh_token\` text NOT NULL,
|
||||||
|
\`token_expiry\` integer,
|
||||||
|
\`active\` integer NOT NULL,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
\`time_updated\` integer NOT NULL,
|
||||||
|
CONSTRAINT \`control_account_pk\` PRIMARY KEY(\`email\`, \`url\`)
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`credential\` (
|
||||||
|
\`id\` text PRIMARY KEY,
|
||||||
|
\`integration_id\` text NOT NULL,
|
||||||
|
\`label\` text NOT NULL,
|
||||||
|
\`value\` text NOT NULL,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
\`time_updated\` integer NOT NULL
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`event_sequence\` (
|
||||||
|
\`aggregate_id\` text PRIMARY KEY,
|
||||||
|
\`seq\` integer NOT NULL,
|
||||||
|
\`owner_id\` text
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`event\` (
|
||||||
|
\`id\` text PRIMARY KEY,
|
||||||
|
\`aggregate_id\` text NOT NULL,
|
||||||
|
\`seq\` integer NOT NULL,
|
||||||
|
\`type\` text NOT NULL,
|
||||||
|
\`data\` text NOT NULL,
|
||||||
|
CONSTRAINT \`fk_event_aggregate_id_event_sequence_aggregate_id_fk\` FOREIGN KEY (\`aggregate_id\`) REFERENCES \`event_sequence\`(\`aggregate_id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`permission\` (
|
||||||
|
\`id\` text PRIMARY KEY,
|
||||||
|
\`project_id\` text NOT NULL,
|
||||||
|
\`action\` text NOT NULL,
|
||||||
|
\`resource\` text NOT NULL,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
\`time_updated\` integer NOT NULL,
|
||||||
|
CONSTRAINT \`fk_permission_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`project_directory\` (
|
||||||
|
\`project_id\` text NOT NULL,
|
||||||
|
\`directory\` text NOT NULL,
|
||||||
|
\`type\` text NOT NULL,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
CONSTRAINT \`project_directory_pk\` PRIMARY KEY(\`project_id\`, \`directory\`),
|
||||||
|
CONSTRAINT \`fk_project_directory_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`project\` (
|
||||||
|
\`id\` text PRIMARY KEY,
|
||||||
|
\`worktree\` text NOT NULL,
|
||||||
|
\`vcs\` text,
|
||||||
|
\`name\` text,
|
||||||
|
\`icon_url\` text,
|
||||||
|
\`icon_url_override\` text,
|
||||||
|
\`icon_color\` text,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
\`time_updated\` integer NOT NULL,
|
||||||
|
\`time_initialized\` integer,
|
||||||
|
\`sandboxes\` text NOT NULL,
|
||||||
|
\`commands\` text
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`message\` (
|
||||||
|
\`id\` text PRIMARY KEY,
|
||||||
|
\`session_id\` text NOT NULL,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
\`time_updated\` integer NOT NULL,
|
||||||
|
\`data\` text NOT NULL,
|
||||||
|
CONSTRAINT \`fk_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`part\` (
|
||||||
|
\`id\` text PRIMARY KEY,
|
||||||
|
\`message_id\` text NOT NULL,
|
||||||
|
\`session_id\` text NOT NULL,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
\`time_updated\` integer NOT NULL,
|
||||||
|
\`data\` text NOT NULL,
|
||||||
|
CONSTRAINT \`fk_part_message_id_message_id_fk\` FOREIGN KEY (\`message_id\`) REFERENCES \`message\`(\`id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`session_context_epoch\` (
|
||||||
|
\`session_id\` text PRIMARY KEY,
|
||||||
|
\`baseline\` text NOT NULL,
|
||||||
|
\`agent\` text DEFAULT 'build' NOT NULL,
|
||||||
|
\`snapshot\` text NOT NULL,
|
||||||
|
\`baseline_seq\` integer NOT NULL,
|
||||||
|
\`replacement_seq\` integer,
|
||||||
|
\`revision\` integer DEFAULT 0 NOT NULL,
|
||||||
|
CONSTRAINT \`fk_session_context_epoch_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`session_input\` (
|
||||||
|
\`id\` text PRIMARY KEY,
|
||||||
|
\`session_id\` text NOT NULL,
|
||||||
|
\`prompt\` text NOT NULL,
|
||||||
|
\`delivery\` text NOT NULL,
|
||||||
|
\`admitted_seq\` integer NOT NULL,
|
||||||
|
\`promoted_seq\` integer,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
CONSTRAINT \`fk_session_input_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`session_message\` (
|
||||||
|
\`id\` text PRIMARY KEY,
|
||||||
|
\`session_id\` text NOT NULL,
|
||||||
|
\`type\` text NOT NULL,
|
||||||
|
\`seq\` integer NOT NULL,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
\`time_updated\` integer NOT NULL,
|
||||||
|
\`data\` text NOT NULL,
|
||||||
|
CONSTRAINT \`fk_session_message_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`session\` (
|
||||||
|
\`id\` text PRIMARY KEY,
|
||||||
|
\`project_id\` text NOT NULL,
|
||||||
|
\`workspace_id\` text,
|
||||||
|
\`parent_id\` text,
|
||||||
|
\`slug\` text NOT NULL,
|
||||||
|
\`directory\` text NOT NULL,
|
||||||
|
\`path\` text,
|
||||||
|
\`title\` text NOT NULL,
|
||||||
|
\`version\` text NOT NULL,
|
||||||
|
\`share_url\` text,
|
||||||
|
\`summary_additions\` integer,
|
||||||
|
\`summary_deletions\` integer,
|
||||||
|
\`summary_files\` integer,
|
||||||
|
\`summary_diffs\` text,
|
||||||
|
\`metadata\` text,
|
||||||
|
\`cost\` real DEFAULT 0 NOT NULL,
|
||||||
|
\`tokens_input\` integer DEFAULT 0 NOT NULL,
|
||||||
|
\`tokens_output\` integer DEFAULT 0 NOT NULL,
|
||||||
|
\`tokens_reasoning\` integer DEFAULT 0 NOT NULL,
|
||||||
|
\`tokens_cache_read\` integer DEFAULT 0 NOT NULL,
|
||||||
|
\`tokens_cache_write\` integer DEFAULT 0 NOT NULL,
|
||||||
|
\`revert\` text,
|
||||||
|
\`permission\` text,
|
||||||
|
\`agent\` text,
|
||||||
|
\`model\` text,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
\`time_updated\` integer NOT NULL,
|
||||||
|
\`time_compacting\` integer,
|
||||||
|
\`time_archived\` integer,
|
||||||
|
CONSTRAINT \`fk_session_project_id_project_id_fk\` FOREIGN KEY (\`project_id\`) REFERENCES \`project\`(\`id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`todo\` (
|
||||||
|
\`session_id\` text NOT NULL,
|
||||||
|
\`content\` text NOT NULL,
|
||||||
|
\`status\` text NOT NULL,
|
||||||
|
\`priority\` text NOT NULL,
|
||||||
|
\`position\` integer NOT NULL,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
\`time_updated\` integer NOT NULL,
|
||||||
|
CONSTRAINT \`todo_pk\` PRIMARY KEY(\`session_id\`, \`position\`),
|
||||||
|
CONSTRAINT \`fk_todo_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`
|
||||||
|
CREATE TABLE \`session_share\` (
|
||||||
|
\`session_id\` text PRIMARY KEY,
|
||||||
|
\`id\` text NOT NULL,
|
||||||
|
\`secret\` text NOT NULL,
|
||||||
|
\`url\` text NOT NULL,
|
||||||
|
\`time_created\` integer NOT NULL,
|
||||||
|
\`time_updated\` integer NOT NULL,
|
||||||
|
CONSTRAINT \`fk_session_share_session_id_session_id_fk\` FOREIGN KEY (\`session_id\`) REFERENCES \`session\`(\`id\`) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`)
|
||||||
|
yield* tx.run(`CREATE UNIQUE INDEX \`event_aggregate_seq_idx\` ON \`event\` (\`aggregate_id\`,\`seq\`);`)
|
||||||
|
yield* tx.run(`CREATE INDEX \`event_aggregate_type_seq_idx\` ON \`event\` (\`aggregate_id\`,\`type\`,\`seq\`);`)
|
||||||
|
yield* tx.run(`CREATE UNIQUE INDEX \`permission_project_action_resource_idx\` ON \`permission\` (\`project_id\`,\`action\`,\`resource\`);`)
|
||||||
|
yield* tx.run(`CREATE INDEX \`message_session_time_created_id_idx\` ON \`message\` (\`session_id\`,\`time_created\`,\`id\`);`)
|
||||||
|
yield* tx.run(`CREATE INDEX \`part_message_id_id_idx\` ON \`part\` (\`message_id\`,\`id\`);`)
|
||||||
|
yield* tx.run(`CREATE INDEX \`part_session_idx\` ON \`part\` (\`session_id\`);`)
|
||||||
|
yield* tx.run(`CREATE INDEX \`session_input_session_pending_delivery_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`,\`delivery\`,\`admitted_seq\`);`)
|
||||||
|
yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_admitted_seq_idx\` ON \`session_input\` (\`session_id\`,\`admitted_seq\`);`)
|
||||||
|
yield* tx.run(`CREATE UNIQUE INDEX \`session_input_session_promoted_seq_idx\` ON \`session_input\` (\`session_id\`,\`promoted_seq\`);`)
|
||||||
|
yield* tx.run(`CREATE UNIQUE INDEX \`session_message_session_seq_idx\` ON \`session_message\` (\`session_id\`,\`seq\`);`)
|
||||||
|
yield* tx.run(`CREATE INDEX \`session_message_session_type_seq_idx\` ON \`session_message\` (\`session_id\`,\`type\`,\`seq\`);`)
|
||||||
|
yield* tx.run(`CREATE INDEX \`session_message_session_time_created_id_idx\` ON \`session_message\` (\`session_id\`,\`time_created\`,\`id\`);`)
|
||||||
|
yield* tx.run(`CREATE INDEX \`session_message_time_created_idx\` ON \`session_message\` (\`time_created\`);`)
|
||||||
|
yield* tx.run(`CREATE INDEX \`session_project_idx\` ON \`session\` (\`project_id\`);`)
|
||||||
|
yield* tx.run(`CREATE INDEX \`session_workspace_idx\` ON \`session\` (\`workspace_id\`);`)
|
||||||
|
yield* tx.run(`CREATE INDEX \`session_parent_idx\` ON \`session\` (\`parent_id\`);`)
|
||||||
|
yield* tx.run(`CREATE INDEX \`todo_session_idx\` ON \`todo\` (\`session_id\`);`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
} satisfies Omit<DatabaseMigration.Migration, "id">
|
||||||
@@ -0,0 +1,524 @@
|
|||||||
|
export * as Integration from "./integration"
|
||||||
|
|
||||||
|
import { Cause, Clock, Context, Duration, Effect, Exit, Layer, Schedule, Schema, Scope, SynchronizedRef } from "effect"
|
||||||
|
import { castDraft, enableMapSet, type Draft } from "immer"
|
||||||
|
import { Credential } from "./credential"
|
||||||
|
import { IntegrationSchema } from "./integration/schema"
|
||||||
|
import { withStatics } from "./schema"
|
||||||
|
import { State } from "./state"
|
||||||
|
import { Identifier } from "./util/identifier"
|
||||||
|
import { EventV2 } from "./event"
|
||||||
|
import { IntegrationConnection } from "./integration/connection"
|
||||||
|
|
||||||
|
export const ID = IntegrationSchema.ID
|
||||||
|
export type ID = IntegrationSchema.ID
|
||||||
|
|
||||||
|
export const MethodID = IntegrationSchema.MethodID
|
||||||
|
export type MethodID = IntegrationSchema.MethodID
|
||||||
|
|
||||||
|
export const AttemptID = Schema.String.pipe(
|
||||||
|
Schema.brand("Integration.AttemptID"),
|
||||||
|
withStatics((schema) => ({ create: () => schema.make("con_" + Identifier.ascending()) })),
|
||||||
|
)
|
||||||
|
export type AttemptID = typeof AttemptID.Type
|
||||||
|
|
||||||
|
export const When = Schema.Struct({
|
||||||
|
key: Schema.String,
|
||||||
|
op: Schema.Literals(["eq", "neq"]),
|
||||||
|
value: Schema.String,
|
||||||
|
}).annotate({ identifier: "Integration.When" })
|
||||||
|
export type When = typeof When.Type
|
||||||
|
|
||||||
|
export class TextPrompt extends Schema.Class<TextPrompt>("Integration.TextPrompt")({
|
||||||
|
type: Schema.Literal("text"),
|
||||||
|
key: Schema.String,
|
||||||
|
message: Schema.String,
|
||||||
|
placeholder: Schema.optional(Schema.String),
|
||||||
|
when: Schema.optional(When),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class SelectPrompt extends Schema.Class<SelectPrompt>("Integration.SelectPrompt")({
|
||||||
|
type: Schema.Literal("select"),
|
||||||
|
key: Schema.String,
|
||||||
|
message: Schema.String,
|
||||||
|
options: Schema.Array(
|
||||||
|
Schema.Struct({
|
||||||
|
label: Schema.String,
|
||||||
|
value: Schema.String,
|
||||||
|
hint: Schema.optional(Schema.String),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
when: Schema.optional(When),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export const Prompt = Schema.Union([TextPrompt, SelectPrompt]).pipe(Schema.toTaggedUnion("type"))
|
||||||
|
export type Prompt = typeof Prompt.Type
|
||||||
|
|
||||||
|
export class OAuthMethod extends Schema.Class<OAuthMethod>("Integration.OAuthMethod")({
|
||||||
|
id: MethodID,
|
||||||
|
type: Schema.Literal("oauth"),
|
||||||
|
label: Schema.String,
|
||||||
|
prompts: Schema.optional(Schema.Array(Prompt)),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class KeyMethod extends Schema.Class<KeyMethod>("Integration.KeyMethod")({
|
||||||
|
type: Schema.Literal("key"),
|
||||||
|
label: Schema.optional(Schema.String),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class EnvMethod extends Schema.Class<EnvMethod>("Integration.EnvMethod")({
|
||||||
|
type: Schema.Literal("env"),
|
||||||
|
names: Schema.Array(Schema.String),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export const Method = Schema.Union([OAuthMethod, KeyMethod, EnvMethod]).pipe(Schema.toTaggedUnion("type"))
|
||||||
|
export type Method = typeof Method.Type
|
||||||
|
|
||||||
|
export class Info extends Schema.Class<Info>("Integration.Info")({
|
||||||
|
id: ID,
|
||||||
|
name: Schema.String,
|
||||||
|
methods: Schema.Array(Method),
|
||||||
|
connections: Schema.Array(IntegrationConnection.Info),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export type Inputs = Readonly<{ [key: string]: string }>
|
||||||
|
|
||||||
|
export type OAuthAuthorization = {
|
||||||
|
readonly url: string
|
||||||
|
readonly instructions: string
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
readonly mode: "auto"
|
||||||
|
readonly callback: Effect.Effect<Credential.Info, unknown>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
readonly mode: "code"
|
||||||
|
readonly callback: (code: string) => Effect.Effect<Credential.Info, unknown>
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface OAuthImplementation {
|
||||||
|
readonly integrationID: ID
|
||||||
|
readonly method: OAuthMethod
|
||||||
|
readonly authorize: (inputs: Inputs) => Effect.Effect<OAuthAuthorization, unknown, Scope.Scope>
|
||||||
|
readonly refresh?: (credential: Credential.OAuth) => Effect.Effect<Credential.OAuth, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyImplementation {
|
||||||
|
readonly integrationID: ID
|
||||||
|
readonly method: KeyMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnvImplementation {
|
||||||
|
readonly integrationID: ID
|
||||||
|
readonly method: EnvMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Implementation = OAuthImplementation | KeyImplementation | EnvImplementation
|
||||||
|
|
||||||
|
function isOAuthImplementation(implementation: Implementation): implementation is OAuthImplementation {
|
||||||
|
return implementation.method.type === "oauth"
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Attempt extends Schema.Class<Attempt>("Integration.Attempt")({
|
||||||
|
attemptID: AttemptID,
|
||||||
|
url: Schema.String,
|
||||||
|
instructions: Schema.String,
|
||||||
|
mode: Schema.Literals(["auto", "code"]),
|
||||||
|
time: Schema.Struct({
|
||||||
|
created: Schema.Number,
|
||||||
|
expires: Schema.Number,
|
||||||
|
}),
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
const Time = Schema.Struct({
|
||||||
|
created: Schema.Number,
|
||||||
|
expires: Schema.Number,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AttemptStatus = Schema.Union([
|
||||||
|
Schema.Struct({ status: Schema.Literal("pending"), time: Time }),
|
||||||
|
Schema.Struct({ status: Schema.Literal("complete"), time: Time }),
|
||||||
|
Schema.Struct({ status: Schema.Literal("failed"), message: Schema.String, time: Time }),
|
||||||
|
Schema.Struct({ status: Schema.Literal("expired"), time: Time }),
|
||||||
|
]).pipe(Schema.toTaggedUnion("status"))
|
||||||
|
export type AttemptStatus = typeof AttemptStatus.Type
|
||||||
|
|
||||||
|
export class CodeRequiredError extends Schema.TaggedErrorClass<CodeRequiredError>()("Integration.CodeRequired", {
|
||||||
|
attemptID: AttemptID,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class AuthorizationError extends Schema.TaggedErrorClass<AuthorizationError>()("Integration.Authorization", {
|
||||||
|
cause: Schema.Defect,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export type Error = CodeRequiredError | AuthorizationError
|
||||||
|
|
||||||
|
export const Event = {
|
||||||
|
Updated: EventV2.define({
|
||||||
|
type: "integration.updated",
|
||||||
|
schema: {},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Ref = {
|
||||||
|
id: ID
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Entry = {
|
||||||
|
ref: Ref
|
||||||
|
methods: Method[]
|
||||||
|
implementations: Map<MethodID, OAuthImplementation>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Data = {
|
||||||
|
integrations: Map<ID, Entry>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Editor = {
|
||||||
|
list: () => readonly Ref[]
|
||||||
|
get: (id: ID) => Ref | undefined
|
||||||
|
update: (id: ID, update: (integration: Draft<Ref>) => void) => void
|
||||||
|
remove: (id: ID) => void
|
||||||
|
method: {
|
||||||
|
list: (integrationID: ID) => readonly Method[]
|
||||||
|
update: (implementation: Implementation) => void
|
||||||
|
remove: (integrationID: ID, method: Method) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Interface {
|
||||||
|
/** Registers a scoped transform over the integration registry. */
|
||||||
|
readonly transform: State.Interface<Data, Editor>["transform"]
|
||||||
|
/** Registers and immediately applies a scoped integration registry update. */
|
||||||
|
readonly update: State.Interface<Data, Editor>["update"]
|
||||||
|
/** Returns one integration with its methods and current connections. */
|
||||||
|
readonly get: (id: ID) => Effect.Effect<Info | undefined>
|
||||||
|
/** Returns all integrations with their methods and current connections. */
|
||||||
|
readonly list: () => Effect.Effect<Info[]>
|
||||||
|
readonly connect: {
|
||||||
|
/** Runs a key method and stores the resulting credential. */
|
||||||
|
readonly key: (input: {
|
||||||
|
/** Integration receiving the credential. */
|
||||||
|
readonly integrationID: ID
|
||||||
|
/** Secret entered by the user. */
|
||||||
|
readonly key: string
|
||||||
|
/** User-facing label for the stored credential. */
|
||||||
|
readonly label?: string
|
||||||
|
}) => Effect.Effect<void, AuthorizationError>
|
||||||
|
/** Starts a stateful OAuth attempt. */
|
||||||
|
readonly oauth: (input: {
|
||||||
|
/** Integration being authenticated. */
|
||||||
|
readonly integrationID: ID
|
||||||
|
/** OAuth method selected by the caller. */
|
||||||
|
readonly methodID: MethodID
|
||||||
|
/** Answers to the method's optional prompts. */
|
||||||
|
readonly inputs: Inputs
|
||||||
|
/** User-facing label for the credential created on completion. */
|
||||||
|
readonly label?: string
|
||||||
|
}) => Effect.Effect<Attempt, AuthorizationError>
|
||||||
|
}
|
||||||
|
readonly attempt: {
|
||||||
|
/** Returns the current state of an OAuth attempt. */
|
||||||
|
readonly status: (attemptID: AttemptID) => Effect.Effect<AttemptStatus>
|
||||||
|
/** Completes the attempt and stores its credential. */
|
||||||
|
readonly complete: (input: {
|
||||||
|
/** Opaque handle returned by `oauth`. */
|
||||||
|
readonly attemptID: AttemptID
|
||||||
|
/** Authorization code required by attempts in code mode. */
|
||||||
|
readonly code?: string
|
||||||
|
}) => Effect.Effect<void, CodeRequiredError | AuthorizationError>
|
||||||
|
/** Cancels an attempt and releases its resources. */
|
||||||
|
readonly cancel: (attemptID: AttemptID) => Effect.Effect<void>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Service extends Context.Service<Service, Interface>()("@opencode/v2/Integration") {}
|
||||||
|
|
||||||
|
enableMapSet()
|
||||||
|
|
||||||
|
const attemptLifetime = Duration.toMillis(Duration.minutes(10))
|
||||||
|
const terminalRetention = Duration.toMillis(Duration.minutes(1))
|
||||||
|
const scrubInterval = Duration.seconds(30)
|
||||||
|
|
||||||
|
type AttemptTime = { created: number; expires: number }
|
||||||
|
type PendingAttempt = {
|
||||||
|
status: "pending"
|
||||||
|
completing: boolean
|
||||||
|
authorization: OAuthAuthorization
|
||||||
|
integrationID: ID
|
||||||
|
methodID: MethodID
|
||||||
|
label?: string
|
||||||
|
scope: Scope.Closeable
|
||||||
|
time: AttemptTime
|
||||||
|
}
|
||||||
|
type TerminalAttempt = {
|
||||||
|
status: "complete" | "failed" | "expired"
|
||||||
|
message?: string
|
||||||
|
removeAt: number
|
||||||
|
time: AttemptTime
|
||||||
|
}
|
||||||
|
type AttemptEntry = PendingAttempt | TerminalAttempt
|
||||||
|
|
||||||
|
export const locationLayer = Layer.effect(
|
||||||
|
Service,
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const credentials = yield* Credential.Service
|
||||||
|
const events = yield* EventV2.Service
|
||||||
|
const scope = yield* Scope.Scope
|
||||||
|
const attempts = SynchronizedRef.makeUnsafe(new Map<AttemptID, AttemptEntry>())
|
||||||
|
const state = State.create<Data, Editor>({
|
||||||
|
initial: () => ({ integrations: new Map<ID, Entry>() }),
|
||||||
|
editor: (draft) => ({
|
||||||
|
list: () => Array.from(draft.integrations.values(), (entry) => entry.ref) as Ref[],
|
||||||
|
get: (id) => draft.integrations.get(id)?.ref as Ref | undefined,
|
||||||
|
update: (id, update) => {
|
||||||
|
const current =
|
||||||
|
draft.integrations.get(id) ??
|
||||||
|
castDraft({ ref: { id, name: id } as Ref, methods: [], implementations: new Map() })
|
||||||
|
if (!draft.integrations.has(id)) draft.integrations.set(id, current)
|
||||||
|
update(current.ref)
|
||||||
|
current.ref.id = id
|
||||||
|
},
|
||||||
|
remove: (id) => draft.integrations.delete(id),
|
||||||
|
method: {
|
||||||
|
list: (integrationID) => (draft.integrations.get(integrationID)?.methods as Method[] | undefined) ?? [],
|
||||||
|
update: (implementation) => {
|
||||||
|
const current =
|
||||||
|
draft.integrations.get(implementation.integrationID) ??
|
||||||
|
castDraft({
|
||||||
|
ref: {
|
||||||
|
id: implementation.integrationID,
|
||||||
|
name: implementation.integrationID,
|
||||||
|
} as Ref,
|
||||||
|
methods: [],
|
||||||
|
implementations: new Map<MethodID, OAuthImplementation>(),
|
||||||
|
})
|
||||||
|
if (!draft.integrations.has(implementation.integrationID)) {
|
||||||
|
draft.integrations.set(implementation.integrationID, current)
|
||||||
|
}
|
||||||
|
const index = current.methods.findIndex((method) => {
|
||||||
|
if (method.type !== implementation.method.type) return false
|
||||||
|
if (method.type !== "oauth" || implementation.method.type !== "oauth") return true
|
||||||
|
return method.id === implementation.method.id
|
||||||
|
})
|
||||||
|
if (index === -1) current.methods.push(castDraft(implementation.method))
|
||||||
|
else current.methods[index] = castDraft(implementation.method)
|
||||||
|
if (isOAuthImplementation(implementation)) {
|
||||||
|
current.implementations.set(implementation.method.id, castDraft(implementation))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remove: (integrationID, method) => {
|
||||||
|
const current = draft.integrations.get(integrationID)
|
||||||
|
if (!current) return
|
||||||
|
const index = current.methods.findIndex((candidate) => {
|
||||||
|
if (candidate.type !== method.type) return false
|
||||||
|
if (candidate.type !== "oauth" || method.type !== "oauth") return true
|
||||||
|
return candidate.id === method.id
|
||||||
|
})
|
||||||
|
if (index !== -1) current.methods.splice(index, 1)
|
||||||
|
if (method.type === "oauth") current.implementations.delete(method.id)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
finalize: () => events.publish(Event.Updated, {}).pipe(Effect.asVoid),
|
||||||
|
})
|
||||||
|
|
||||||
|
const connections = (
|
||||||
|
entry: Entry,
|
||||||
|
saved: readonly Credential.Stored[],
|
||||||
|
): IntegrationConnection.Info[] => {
|
||||||
|
const connected = saved.map(
|
||||||
|
(credential) =>
|
||||||
|
new IntegrationConnection.CredentialInfo({ type: "credential", id: credential.id, label: credential.label }),
|
||||||
|
)
|
||||||
|
const detected = entry.methods
|
||||||
|
.filter((method) => method.type === "env")
|
||||||
|
.flatMap((method) => method.names.filter((name) => process.env[name]))
|
||||||
|
.map(
|
||||||
|
(name, index) =>
|
||||||
|
new IntegrationConnection.EnvInfo({
|
||||||
|
type: "env",
|
||||||
|
name,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return [...connected, ...detected]
|
||||||
|
}
|
||||||
|
|
||||||
|
const project = (entry: Entry, saved: readonly Credential.Stored[]) =>
|
||||||
|
new Info({
|
||||||
|
id: entry.ref.id,
|
||||||
|
name: entry.ref.name,
|
||||||
|
methods: entry.methods,
|
||||||
|
connections: connections(entry, saved),
|
||||||
|
})
|
||||||
|
|
||||||
|
const authorize = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
||||||
|
effect.pipe(Effect.mapError((cause) => new AuthorizationError({ cause })))
|
||||||
|
|
||||||
|
const close = (attemptScope: Scope.Closeable) =>
|
||||||
|
Scope.close(attemptScope, Exit.void).pipe(Effect.forkIn(scope, { startImmediately: true }), Effect.asVoid)
|
||||||
|
|
||||||
|
const message = (cause: Cause.Cause<unknown>) => {
|
||||||
|
const error = Cause.squash(cause)
|
||||||
|
return error instanceof Error ? error.message : String(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const settle = Effect.fnUntraced(function* (attemptID: AttemptID, exit: Exit.Exit<Credential.Info, unknown>) {
|
||||||
|
const now = yield* Clock.currentTimeMillis
|
||||||
|
const result = yield* SynchronizedRef.modify(attempts, (current) => {
|
||||||
|
const attempt = current.get(attemptID)
|
||||||
|
if (!attempt || attempt.status !== "pending") return [undefined, current]
|
||||||
|
const terminal: TerminalAttempt = Exit.isSuccess(exit)
|
||||||
|
? { status: "complete", time: attempt.time, removeAt: now + terminalRetention }
|
||||||
|
: { status: "failed", message: message(exit.cause), time: attempt.time, removeAt: now + terminalRetention }
|
||||||
|
return [attempt, new Map(current).set(attemptID, terminal)]
|
||||||
|
})
|
||||||
|
if (!result) return
|
||||||
|
if (Exit.isSuccess(exit)) {
|
||||||
|
yield* credentials.create({
|
||||||
|
integrationID: result.integrationID,
|
||||||
|
label: result.label,
|
||||||
|
value:
|
||||||
|
exit.value.type === "oauth"
|
||||||
|
? new Credential.OAuth({ ...exit.value, methodID: result.methodID })
|
||||||
|
: exit.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
yield* close(result.scope)
|
||||||
|
})
|
||||||
|
|
||||||
|
const scrub = Effect.fnUntraced(function* () {
|
||||||
|
const now = yield* Clock.currentTimeMillis
|
||||||
|
const expired = yield* SynchronizedRef.modify(attempts, (current) => {
|
||||||
|
const next = new Map(current)
|
||||||
|
const scopes: Scope.Closeable[] = []
|
||||||
|
for (const [id, attempt] of current) {
|
||||||
|
if (attempt.status === "pending" && attempt.time.expires <= now) {
|
||||||
|
scopes.push(attempt.scope)
|
||||||
|
next.set(id, { status: "expired", time: attempt.time, removeAt: now + terminalRetention })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (attempt.status !== "pending" && attempt.removeAt <= now) next.delete(id)
|
||||||
|
}
|
||||||
|
return [scopes, next]
|
||||||
|
})
|
||||||
|
yield* Effect.forEach(expired, close, { discard: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* scrub().pipe(Effect.repeat(Schedule.spaced(scrubInterval)), Effect.forkIn(scope))
|
||||||
|
|
||||||
|
return Service.of({
|
||||||
|
transform: state.transform,
|
||||||
|
update: state.update,
|
||||||
|
get: Effect.fn("Integration.get")(function* (id) {
|
||||||
|
const entry = state.get().integrations.get(id)
|
||||||
|
if (!entry) return undefined
|
||||||
|
return project(entry, yield* credentials.list(id))
|
||||||
|
}),
|
||||||
|
list: Effect.fn("Integration.list")(function* () {
|
||||||
|
return (yield* Effect.forEach(state.get().integrations.values(), (entry) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
return project(entry, yield* credentials.list(entry.ref.id))
|
||||||
|
}),
|
||||||
|
)).toSorted((a, b) => a.name.localeCompare(b.name))
|
||||||
|
}),
|
||||||
|
connect: {
|
||||||
|
key: Effect.fn("Integration.connect.key")(function* (input) {
|
||||||
|
const method = state
|
||||||
|
.get()
|
||||||
|
.integrations.get(input.integrationID)
|
||||||
|
?.methods.some((method) => method.type === "key")
|
||||||
|
if (!method) return yield* Effect.die(`Key method not found: ${input.integrationID}`)
|
||||||
|
yield* credentials.create({
|
||||||
|
integrationID: input.integrationID,
|
||||||
|
label: input.label,
|
||||||
|
value: new Credential.Key({ type: "key", key: input.key }),
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
oauth: Effect.fn("Integration.connect.oauth")(function* (input) {
|
||||||
|
const method = state.get().integrations.get(input.integrationID)?.implementations.get(input.methodID)
|
||||||
|
if (!method) {
|
||||||
|
return yield* Effect.die(`OAuth method not found: ${input.integrationID}/${input.methodID}`)
|
||||||
|
}
|
||||||
|
const attemptScope = yield* Scope.fork(scope)
|
||||||
|
const authorization = yield* authorize(method.authorize(input.inputs)).pipe(
|
||||||
|
Scope.provide(attemptScope),
|
||||||
|
Effect.onExit((exit) => (Exit.isFailure(exit) ? Scope.close(attemptScope, exit) : Effect.void)),
|
||||||
|
)
|
||||||
|
const id = AttemptID.create()
|
||||||
|
const created = yield* Clock.currentTimeMillis
|
||||||
|
const time = { created, expires: created + attemptLifetime }
|
||||||
|
yield* SynchronizedRef.update(attempts, (current) =>
|
||||||
|
new Map(current).set(id, {
|
||||||
|
status: "pending",
|
||||||
|
completing: authorization.mode === "auto",
|
||||||
|
authorization,
|
||||||
|
integrationID: input.integrationID,
|
||||||
|
methodID: input.methodID,
|
||||||
|
label: input.label,
|
||||||
|
scope: attemptScope,
|
||||||
|
time,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if (authorization.mode === "auto") {
|
||||||
|
yield* authorization.callback.pipe(
|
||||||
|
Effect.exit,
|
||||||
|
Effect.flatMap((exit) => settle(id, exit)),
|
||||||
|
Effect.forkIn(attemptScope, { startImmediately: true }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return new Attempt({
|
||||||
|
attemptID: id,
|
||||||
|
url: authorization.url,
|
||||||
|
instructions: authorization.instructions,
|
||||||
|
mode: authorization.mode,
|
||||||
|
time,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
attempt: {
|
||||||
|
status: Effect.fn("Integration.attempt.status")(function* (attemptID) {
|
||||||
|
const attempt = (yield* SynchronizedRef.get(attempts)).get(attemptID)
|
||||||
|
if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${attemptID}`)
|
||||||
|
if (attempt.status === "failed") {
|
||||||
|
return { status: attempt.status, message: attempt.message ?? "Authorization failed", time: attempt.time }
|
||||||
|
}
|
||||||
|
return { status: attempt.status, time: attempt.time }
|
||||||
|
}),
|
||||||
|
complete: Effect.fn("Integration.attempt.complete")(function* (input) {
|
||||||
|
const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
|
||||||
|
const match = current.get(input.attemptID)
|
||||||
|
if (!match || match.status !== "pending" || match.completing) return [match, current]
|
||||||
|
if (match.authorization.mode === "code" && input.code === undefined) return [match, current]
|
||||||
|
return [match, new Map(current).set(input.attemptID, { ...match, completing: true })]
|
||||||
|
})
|
||||||
|
if (!attempt) return yield* Effect.die(`OAuth attempt not found: ${input.attemptID}`)
|
||||||
|
if (attempt.status !== "pending") return
|
||||||
|
if (attempt.authorization.mode === "code" && input.code === undefined) {
|
||||||
|
return yield* new CodeRequiredError({ attemptID: input.attemptID })
|
||||||
|
}
|
||||||
|
if (attempt.completing) return yield* Effect.die(`OAuth attempt already completing: ${input.attemptID}`)
|
||||||
|
const callback =
|
||||||
|
attempt.authorization.mode === "auto"
|
||||||
|
? attempt.authorization.callback
|
||||||
|
: attempt.authorization.callback(input.code as string)
|
||||||
|
const exit = yield* authorize(callback).pipe(Effect.exit)
|
||||||
|
yield* settle(input.attemptID, exit)
|
||||||
|
if (Exit.isFailure(exit)) return yield* exit
|
||||||
|
}),
|
||||||
|
cancel: Effect.fn("Integration.attempt.cancel")(function* (attemptID) {
|
||||||
|
const attempt = yield* SynchronizedRef.modify(attempts, (current) => {
|
||||||
|
const match = current.get(attemptID)
|
||||||
|
if (!match || match.status !== "pending") return [undefined, current]
|
||||||
|
const next = new Map(current)
|
||||||
|
next.delete(attemptID)
|
||||||
|
return [match, next]
|
||||||
|
})
|
||||||
|
if (attempt) yield* Scope.close(attempt.scope, Exit.void)
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export * as IntegrationConnection from "./connection"
|
||||||
|
|
||||||
|
import { Schema } from "effect"
|
||||||
|
import { Credential } from "../credential"
|
||||||
|
|
||||||
|
export class CredentialInfo extends Schema.Class<CredentialInfo>("Connection.CredentialInfo")({
|
||||||
|
type: Schema.Literal("credential"),
|
||||||
|
id: Credential.ID,
|
||||||
|
label: Schema.String,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export class EnvInfo extends Schema.Class<EnvInfo>("Connection.EnvInfo")({
|
||||||
|
type: Schema.Literal("env"),
|
||||||
|
name: Schema.String,
|
||||||
|
}) {}
|
||||||
|
|
||||||
|
export const Info = Schema.Union([CredentialInfo, EnvInfo])
|
||||||
|
.pipe(Schema.toTaggedUnion("type"))
|
||||||
|
.annotate({ identifier: "Connection.Info" })
|
||||||
|
export type Info = typeof Info.Type
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export * as IntegrationSchema from "./schema"
|
||||||
|
|
||||||
|
import { Schema } from "effect"
|
||||||
|
|
||||||
|
export const ID = Schema.String.pipe(Schema.brand("Integration.ID"))
|
||||||
|
export type ID = typeof ID.Type
|
||||||
|
|
||||||
|
export const MethodID = Schema.String.pipe(Schema.brand("Integration.MethodID"))
|
||||||
|
export type MethodID = typeof MethodID.Type
|
||||||
@@ -4,7 +4,7 @@ import { Policy } from "./policy"
|
|||||||
import { Config } from "./config"
|
import { Config } from "./config"
|
||||||
import { PluginV2 } from "./plugin"
|
import { PluginV2 } from "./plugin"
|
||||||
import { Catalog } from "./catalog"
|
import { Catalog } from "./catalog"
|
||||||
import { Connector } from "./connector"
|
import { Integration } from "./integration"
|
||||||
import { CommandV2 } from "./command"
|
import { CommandV2 } from "./command"
|
||||||
import { AgentV2 } from "./agent"
|
import { AgentV2 } from "./agent"
|
||||||
import { PluginBoot } from "./plugin/boot"
|
import { PluginBoot } from "./plugin/boot"
|
||||||
@@ -59,7 +59,7 @@ export class LocationServiceMap extends LayerMap.Service<LocationServiceMap>()("
|
|||||||
Reference.locationLayer,
|
Reference.locationLayer,
|
||||||
PluginV2.locationLayer,
|
PluginV2.locationLayer,
|
||||||
Catalog.locationLayer,
|
Catalog.locationLayer,
|
||||||
Connector.locationLayer,
|
Integration.locationLayer,
|
||||||
CommandV2.locationLayer,
|
CommandV2.locationLayer,
|
||||||
AgentV2.locationLayer,
|
AgentV2.locationLayer,
|
||||||
PluginBoot.locationLayer,
|
PluginBoot.locationLayer,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export * as PluginBoot from "./boot"
|
|||||||
|
|
||||||
import { Context, Deferred, Effect, Layer } from "effect"
|
import { Context, Deferred, Effect, Layer } from "effect"
|
||||||
import { Credential } from "../credential"
|
import { Credential } from "../credential"
|
||||||
import { Connector } from "../connector"
|
import { Integration } from "../integration"
|
||||||
import { AgentV2 } from "../agent"
|
import { AgentV2 } from "../agent"
|
||||||
import { Catalog } from "../catalog"
|
import { Catalog } from "../catalog"
|
||||||
import { CommandV2 } from "../command"
|
import { CommandV2 } from "../command"
|
||||||
@@ -34,7 +34,7 @@ type Plugin = {
|
|||||||
| Catalog.Service
|
| Catalog.Service
|
||||||
| CommandV2.Service
|
| CommandV2.Service
|
||||||
| Credential.Service
|
| Credential.Service
|
||||||
| Connector.Service
|
| Integration.Service
|
||||||
| AgentV2.Service
|
| AgentV2.Service
|
||||||
| Npm.Service
|
| Npm.Service
|
||||||
| EventV2.Service
|
| EventV2.Service
|
||||||
@@ -62,7 +62,7 @@ export const layer = Layer.effect(
|
|||||||
const commands = yield* CommandV2.Service
|
const commands = yield* CommandV2.Service
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const credentials = yield* Credential.Service
|
const credentials = yield* Credential.Service
|
||||||
const connectors = yield* Connector.Service
|
const integrations = yield* Integration.Service
|
||||||
const agents = yield* AgentV2.Service
|
const agents = yield* AgentV2.Service
|
||||||
const config = yield* Config.Service
|
const config = yield* Config.Service
|
||||||
const location = yield* Location.Service
|
const location = yield* Location.Service
|
||||||
@@ -82,7 +82,7 @@ export const layer = Layer.effect(
|
|||||||
Effect.provideService(Catalog.Service, catalog),
|
Effect.provideService(Catalog.Service, catalog),
|
||||||
Effect.provideService(CommandV2.Service, commands),
|
Effect.provideService(CommandV2.Service, commands),
|
||||||
Effect.provideService(Credential.Service, credentials),
|
Effect.provideService(Credential.Service, credentials),
|
||||||
Effect.provideService(Connector.Service, connectors),
|
Effect.provideService(Integration.Service, integrations),
|
||||||
Effect.provideService(AgentV2.Service, agents),
|
Effect.provideService(AgentV2.Service, agents),
|
||||||
Effect.provideService(Config.Service, config),
|
Effect.provideService(Config.Service, config),
|
||||||
Effect.provideService(Location.Service, location),
|
Effect.provideService(Location.Service, location),
|
||||||
@@ -127,7 +127,7 @@ export const layer = Layer.effect(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const locationLayer = layer.pipe(
|
export const locationLayer = layer.pipe(
|
||||||
Layer.provideMerge(Connector.locationLayer),
|
Layer.provideMerge(Integration.locationLayer),
|
||||||
Layer.provideMerge(Catalog.locationLayer),
|
Layer.provideMerge(Catalog.locationLayer),
|
||||||
Layer.provideMerge(CommandV2.locationLayer),
|
Layer.provideMerge(CommandV2.locationLayer),
|
||||||
Layer.provideMerge(Config.locationLayer),
|
Layer.provideMerge(Config.locationLayer),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { DateTime, Effect, Scope, Stream } from "effect"
|
import { DateTime, Effect, Scope, Stream } from "effect"
|
||||||
import { Catalog } from "../catalog"
|
import { Catalog } from "../catalog"
|
||||||
import { Connector } from "../connector"
|
import { Integration } from "../integration"
|
||||||
import { Credential } from "../credential"
|
|
||||||
import { EventV2 } from "../event"
|
import { EventV2 } from "../event"
|
||||||
import { ModelV2 } from "../model"
|
import { ModelV2 } from "../model"
|
||||||
import { ModelRequest } from "../model-request"
|
import { ModelRequest } from "../model-request"
|
||||||
@@ -56,27 +55,31 @@ export const ModelsDevPlugin = PluginV2.define({
|
|||||||
id: PluginV2.ID.make("models-dev"),
|
id: PluginV2.ID.make("models-dev"),
|
||||||
effect: Effect.gen(function* () {
|
effect: Effect.gen(function* () {
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
const connectors = yield* Connector.Service
|
const integrations = yield* Integration.Service
|
||||||
const modelsDev = yield* ModelsDev.Service
|
const modelsDev = yield* ModelsDev.Service
|
||||||
const events = yield* EventV2.Service
|
const events = yield* EventV2.Service
|
||||||
const scope = yield* Scope.Scope
|
const scope = yield* Scope.Scope
|
||||||
const transform = yield* catalog.transform()
|
const transform = yield* catalog.transform()
|
||||||
const connectorTransform = yield* connectors.transform()
|
const integrationTransform = yield* integrations.transform()
|
||||||
const refresh = Effect.fn("ModelsDevPlugin.refresh")(function* () {
|
const refresh = Effect.fn("ModelsDevPlugin.refresh")(function* () {
|
||||||
const data = yield* modelsDev.get()
|
const data = yield* modelsDev.get()
|
||||||
yield* connectorTransform((connectors) => {
|
yield* integrationTransform((integrations) => {
|
||||||
for (const item of Object.values(data)) {
|
for (const item of Object.values(data)) {
|
||||||
if (item.env.length === 0) continue
|
if (item.env.length === 0) continue
|
||||||
const connectorID = Connector.ID.make(item.id)
|
const integrationID = Integration.ID.make(item.id)
|
||||||
connectors.update(connectorID, (connector) => (connector.name = item.name))
|
integrations.update(integrationID, (integration) => (integration.name = item.name))
|
||||||
connectors.method.update({
|
integrations.method.update({
|
||||||
connectorID,
|
integrationID,
|
||||||
method: new Connector.KeyMethod({
|
method: new Integration.KeyMethod({
|
||||||
id: Connector.MethodID.make("api-key"),
|
|
||||||
type: "key",
|
type: "key",
|
||||||
label: "API Key",
|
|
||||||
}),
|
}),
|
||||||
authorize: (key: string) => Effect.succeed(new Credential.Key({ type: "key", key })),
|
})
|
||||||
|
integrations.method.update({
|
||||||
|
integrationID,
|
||||||
|
method: new Integration.EnvMethod({
|
||||||
|
type: "env",
|
||||||
|
names: [...item.env],
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createServer } from "node:http"
|
import { createServer } from "node:http"
|
||||||
import { Deferred, Effect } from "effect"
|
import { Deferred, Effect } from "effect"
|
||||||
import { Connector } from "../../connector"
|
import { Integration } from "../../integration"
|
||||||
import { Credential } from "../../credential"
|
import { Credential } from "../../credential"
|
||||||
import { InstallationVersion } from "../../installation/version"
|
import { InstallationVersion } from "../../installation/version"
|
||||||
|
|
||||||
@@ -27,10 +27,13 @@ type Claims = {
|
|||||||
"https://api.openai.com/auth"?: { chatgpt_account_id?: string }
|
"https://api.openai.com/auth"?: { chatgpt_account_id?: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const browserMethodID = Integration.MethodID.make("chatgpt-browser")
|
||||||
|
const headlessMethodID = Integration.MethodID.make("chatgpt-headless")
|
||||||
|
|
||||||
export const browser = {
|
export const browser = {
|
||||||
connectorID: Connector.ID.make("openai"),
|
integrationID: Integration.ID.make("openai"),
|
||||||
method: new Connector.OAuthMethod({
|
method: new Integration.OAuthMethod({
|
||||||
id: Connector.MethodID.make("chatgpt-browser"),
|
id: browserMethodID,
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
label: "ChatGPT Pro/Plus (browser)",
|
label: "ChatGPT Pro/Plus (browser)",
|
||||||
}),
|
}),
|
||||||
@@ -77,17 +80,17 @@ export const browser = {
|
|||||||
instructions: "Complete authorization in your browser. This window will close automatically.",
|
instructions: "Complete authorization in your browser. This window will close automatically.",
|
||||||
callback: Deferred.await(code).pipe(
|
callback: Deferred.await(code).pipe(
|
||||||
Effect.flatMap((value) => exchange(value, redirect, pkce)),
|
Effect.flatMap((value) => exchange(value, redirect, pkce)),
|
||||||
Effect.map(credential),
|
Effect.map((tokens) => credential(browserMethodID, tokens)),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
refresh: (value) => refresh(value),
|
refresh: (value) => refresh(value),
|
||||||
} satisfies Connector.OAuthImplementation
|
} satisfies Integration.OAuthImplementation
|
||||||
|
|
||||||
export const headless = {
|
export const headless = {
|
||||||
connectorID: Connector.ID.make("openai"),
|
integrationID: Integration.ID.make("openai"),
|
||||||
method: new Connector.OAuthMethod({
|
method: new Integration.OAuthMethod({
|
||||||
id: Connector.MethodID.make("chatgpt-headless"),
|
id: headlessMethodID,
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
label: "ChatGPT Pro/Plus (headless)",
|
label: "ChatGPT Pro/Plus (headless)",
|
||||||
}),
|
}),
|
||||||
@@ -124,6 +127,7 @@ export const headless = {
|
|||||||
code_verifier: string
|
code_verifier: string
|
||||||
}
|
}
|
||||||
return credential(
|
return credential(
|
||||||
|
headlessMethodID,
|
||||||
yield* exchange(data.authorization_code, `${issuer}/deviceauth/callback`, {
|
yield* exchange(data.authorization_code, `${issuer}/deviceauth/callback`, {
|
||||||
verifier: data.code_verifier,
|
verifier: data.code_verifier,
|
||||||
challenge: "",
|
challenge: "",
|
||||||
@@ -139,7 +143,7 @@ export const headless = {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
refresh: (value) => refresh(value),
|
refresh: (value) => refresh(value),
|
||||||
} satisfies Connector.OAuthImplementation
|
} satisfies Integration.OAuthImplementation
|
||||||
|
|
||||||
function headers(contentType: string) {
|
function headers(contentType: string) {
|
||||||
return { "Content-Type": contentType, "User-Agent": `opencode/${InstallationVersion}` }
|
return { "Content-Type": contentType, "User-Agent": `opencode/${InstallationVersion}` }
|
||||||
@@ -170,7 +174,7 @@ function refresh(value: Credential.OAuth) {
|
|||||||
}).toString(),
|
}).toString(),
|
||||||
}).pipe(
|
}).pipe(
|
||||||
Effect.map((tokens) => {
|
Effect.map((tokens) => {
|
||||||
const next = credential(tokens)
|
const next = credential(value.methodID, tokens)
|
||||||
return new Credential.OAuth({
|
return new Credential.OAuth({
|
||||||
...next,
|
...next,
|
||||||
metadata: next.metadata ?? value.metadata,
|
metadata: next.metadata ?? value.metadata,
|
||||||
@@ -190,10 +194,11 @@ function request<A>(url: string, init: RequestInit) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function credential(tokens: TokenResponse) {
|
function credential(methodID: Integration.MethodID, tokens: TokenResponse) {
|
||||||
const accountID = extractAccountID(tokens)
|
const accountID = extractAccountID(tokens)
|
||||||
return new Credential.OAuth({
|
return new Credential.OAuth({
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
|
methodID,
|
||||||
refresh: tokens.refresh_token,
|
refresh: tokens.refresh_token,
|
||||||
access: tokens.access_token,
|
access: tokens.access_token,
|
||||||
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ import { Effect } from "effect"
|
|||||||
import { ModelV2 } from "../../model"
|
import { ModelV2 } from "../../model"
|
||||||
import { PluginV2 } from "../../plugin"
|
import { PluginV2 } from "../../plugin"
|
||||||
import { ProviderV2 } from "../../provider"
|
import { ProviderV2 } from "../../provider"
|
||||||
import { Connector } from "../../connector"
|
import { Integration } from "../../integration"
|
||||||
import { browser, headless } from "./openai-auth"
|
import { browser, headless } from "./openai-auth"
|
||||||
|
|
||||||
export const OpenAIPlugin = PluginV2.define({
|
export const OpenAIPlugin = PluginV2.define({
|
||||||
id: PluginV2.ID.make("openai"),
|
id: PluginV2.ID.make("openai"),
|
||||||
effect: Effect.gen(function* () {
|
effect: Effect.gen(function* () {
|
||||||
const connectors = yield* Connector.Service
|
const integrations = yield* Integration.Service
|
||||||
yield* connectors.update((editor) => {
|
yield* integrations.update((editor) => {
|
||||||
editor.method.update(browser)
|
editor.method.update(browser)
|
||||||
editor.method.update(headless)
|
editor.method.update(headless)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { DateTime, Effect, Layer, Option } from "effect"
|
import { DateTime, Effect, Layer, Option } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Connector } from "@opencode-ai/core/connector"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
@@ -22,35 +22,38 @@ const it = testEffect(
|
|||||||
Catalog.locationLayer.pipe(
|
Catalog.locationLayer.pipe(
|
||||||
Layer.provideMerge(EventV2.defaultLayer),
|
Layer.provideMerge(EventV2.defaultLayer),
|
||||||
Layer.provideMerge(locationLayer),
|
Layer.provideMerge(locationLayer),
|
||||||
Layer.provideMerge(Layer.mock(Credential.Service)({ activeAll: () => Effect.succeed(new Map()) })),
|
Layer.provideMerge(
|
||||||
|
Layer.mock(Credential.Service)({
|
||||||
|
all: () => Effect.succeed([]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
describe("CatalogV2", () => {
|
describe("CatalogV2", () => {
|
||||||
it.effect("projects active credentials without rebuilding catalog state", () => {
|
it.effect("projects active credentials without rebuilding catalog state", () => {
|
||||||
const connectorID = Connector.ID.make("test")
|
const integrationID = Integration.ID.make("test")
|
||||||
const methodID = Connector.MethodID.make("api-key")
|
const first = {
|
||||||
const first = new Credential.Info({
|
|
||||||
id: Credential.ID.create(),
|
id: Credential.ID.create(),
|
||||||
connectorID,
|
integrationID,
|
||||||
methodID,
|
|
||||||
label: "First",
|
label: "First",
|
||||||
value: new Credential.Key({ type: "key", key: "first", metadata: { tenant: "one" } }),
|
value: new Credential.Key({ type: "key", key: "first", metadata: { tenant: "one" } }),
|
||||||
})
|
}
|
||||||
const second = new Credential.Info({
|
const second = {
|
||||||
id: Credential.ID.create(),
|
id: Credential.ID.create(),
|
||||||
connectorID,
|
integrationID,
|
||||||
methodID,
|
|
||||||
label: "Second",
|
label: "Second",
|
||||||
value: new Credential.Key({ type: "key", key: "second", metadata: { tenant: "two" } }),
|
value: new Credential.Key({ type: "key", key: "second", metadata: { tenant: "two" } }),
|
||||||
})
|
}
|
||||||
let active = first
|
let active = first
|
||||||
const layer = Catalog.locationLayer.pipe(
|
const layer = Catalog.locationLayer.pipe(
|
||||||
Layer.fresh,
|
Layer.fresh,
|
||||||
Layer.provideMerge(EventV2.defaultLayer),
|
Layer.provideMerge(EventV2.defaultLayer),
|
||||||
Layer.provideMerge(locationLayer),
|
Layer.provideMerge(locationLayer),
|
||||||
Layer.provideMerge(
|
Layer.provideMerge(
|
||||||
Layer.mock(Credential.Service)({ activeAll: () => Effect.succeed(new Map([[connectorID, active]])) }),
|
Layer.mock(Credential.Service)({
|
||||||
|
all: () => Effect.sync(() => [active]),
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,401 +0,0 @@
|
|||||||
import { describe, expect } from "bun:test"
|
|
||||||
import { Duration, Effect, Exit, Layer, Scope } from "effect"
|
|
||||||
import * as TestClock from "effect/testing/TestClock"
|
|
||||||
import { Connector } from "@opencode-ai/core/connector"
|
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
|
||||||
import { it } from "./lib/effect"
|
|
||||||
|
|
||||||
const layer = Connector.locationLayer.pipe(
|
|
||||||
Layer.provide(EventV2.defaultLayer),
|
|
||||||
Layer.provide(
|
|
||||||
Layer.mock(Credential.Service)({
|
|
||||||
create: () => Effect.die("unexpected credential creation"),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
function connectionLayer(
|
|
||||||
created: Array<{
|
|
||||||
connectorID: Connector.ID
|
|
||||||
methodID: Connector.MethodID
|
|
||||||
label?: string
|
|
||||||
value: Credential.Value
|
|
||||||
}>,
|
|
||||||
) {
|
|
||||||
return Connector.locationLayer.pipe(
|
|
||||||
Layer.provide(EventV2.defaultLayer),
|
|
||||||
Layer.provide(
|
|
||||||
Layer.mock(Credential.Service)({
|
|
||||||
create: (input) =>
|
|
||||||
Effect.sync(() => {
|
|
||||||
created.push(input)
|
|
||||||
return new Credential.Info({ id: Credential.ID.create(), ...input, label: input.label ?? "default" })
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Connector", () => {
|
|
||||||
it.effect("registers connectors through the editor", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const connectors = yield* Connector.Service
|
|
||||||
const scope = yield* Scope.fork(yield* Scope.Scope)
|
|
||||||
const openai = Connector.ID.make("openai")
|
|
||||||
|
|
||||||
yield* connectors
|
|
||||||
.update((editor) => editor.update(openai, (connector) => (connector.name = "OpenAI")))
|
|
||||||
.pipe(Scope.provide(scope))
|
|
||||||
expect(yield* connectors.get(openai)).toEqual(new Connector.Info({ id: openai, name: "OpenAI", methods: [] }))
|
|
||||||
|
|
||||||
yield* Scope.close(scope, Exit.void)
|
|
||||||
expect(yield* connectors.get(openai)).toBeUndefined()
|
|
||||||
}).pipe(Effect.provide(layer)),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("reveals the previous registration when an override closes", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const connectors = yield* Connector.Service
|
|
||||||
const id = Connector.ID.make("openai")
|
|
||||||
const first = yield* Scope.fork(yield* Scope.Scope)
|
|
||||||
const second = yield* Scope.fork(yield* Scope.Scope)
|
|
||||||
|
|
||||||
yield* connectors
|
|
||||||
.update((editor) => editor.update(id, (connector) => (connector.name = "OpenAI")))
|
|
||||||
.pipe(Scope.provide(first))
|
|
||||||
yield* connectors
|
|
||||||
.update((editor) => editor.update(id, (connector) => (connector.name = "OpenAI Override")))
|
|
||||||
.pipe(Scope.provide(second))
|
|
||||||
expect((yield* connectors.get(id))?.name).toBe("OpenAI Override")
|
|
||||||
|
|
||||||
yield* Scope.close(second, Exit.void)
|
|
||||||
expect((yield* connectors.get(id))?.name).toBe("OpenAI")
|
|
||||||
expect((yield* connectors.list()).map((connector) => connector.id)).toEqual([id])
|
|
||||||
}).pipe(Effect.provide(layer)),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("registers and overrides methods independently", () =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const connectors = yield* Connector.Service
|
|
||||||
const connectorID = Connector.ID.make("openai")
|
|
||||||
const methodID = Connector.MethodID.make("chatgpt")
|
|
||||||
const first = yield* Scope.fork(yield* Scope.Scope)
|
|
||||||
const second = yield* Scope.fork(yield* Scope.Scope)
|
|
||||||
const authorize = () =>
|
|
||||||
Effect.succeed({
|
|
||||||
mode: "auto" as const,
|
|
||||||
url: "https://example.com/authorize",
|
|
||||||
instructions: "Sign in",
|
|
||||||
callback: Effect.never,
|
|
||||||
})
|
|
||||||
|
|
||||||
yield* connectors
|
|
||||||
.update((editor) =>
|
|
||||||
editor.method.update({
|
|
||||||
connectorID,
|
|
||||||
method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }),
|
|
||||||
authorize,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.pipe(Scope.provide(first))
|
|
||||||
yield* connectors
|
|
||||||
.update((editor) =>
|
|
||||||
editor.method.update({
|
|
||||||
connectorID,
|
|
||||||
method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT Override" }),
|
|
||||||
authorize,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.pipe(Scope.provide(second))
|
|
||||||
|
|
||||||
expect((yield* connectors.get(connectorID))?.name).toBe("openai")
|
|
||||||
expect((yield* connectors.get(connectorID))?.methods[0]?.label).toBe("ChatGPT Override")
|
|
||||||
|
|
||||||
yield* Scope.close(second, Exit.void)
|
|
||||||
expect((yield* connectors.get(connectorID))?.methods[0]?.label).toBe("ChatGPT")
|
|
||||||
expect((yield* connectors.get(connectorID))?.methods.map((method) => method.id)).toEqual([methodID])
|
|
||||||
}).pipe(Effect.provide(layer)),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.effect("connects with a key and stores the credential", () => {
|
|
||||||
const created: Array<{
|
|
||||||
connectorID: Connector.ID
|
|
||||||
methodID: Connector.MethodID
|
|
||||||
label?: string
|
|
||||||
value: Credential.Value
|
|
||||||
}> = []
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const connectors = yield* Connector.Service
|
|
||||||
const connectorID = Connector.ID.make("openai")
|
|
||||||
const methodID = Connector.MethodID.make("api-key")
|
|
||||||
yield* connectors.update((editor) =>
|
|
||||||
editor.method.update({
|
|
||||||
connectorID,
|
|
||||||
method: new Connector.KeyMethod({ id: methodID, type: "key", label: "API key" }),
|
|
||||||
authorize: (key, inputs) =>
|
|
||||||
Effect.succeed(
|
|
||||||
new Credential.Key({ type: "key", key, metadata: { organization: inputs.organization ?? "" } }),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* connectors.connect.key({
|
|
||||||
connectorID,
|
|
||||||
methodID,
|
|
||||||
key: "secret",
|
|
||||||
inputs: { organization: "acme" },
|
|
||||||
label: "Work",
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(created).toEqual([
|
|
||||||
{
|
|
||||||
connectorID,
|
|
||||||
methodID,
|
|
||||||
label: "Work",
|
|
||||||
value: new Credential.Key({ type: "key", key: "secret", metadata: { organization: "acme" } }),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}).pipe(Effect.provide(connectionLayer(created)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it.effect("refreshes OAuth with the originating method", () => {
|
|
||||||
const connectorID = Connector.ID.make("openai")
|
|
||||||
const methodID = Connector.MethodID.make("chatgpt")
|
|
||||||
const credentialID = Credential.ID.create()
|
|
||||||
const current = new Credential.OAuth({
|
|
||||||
type: "oauth",
|
|
||||||
access: "old-access",
|
|
||||||
refresh: "old-refresh",
|
|
||||||
expires: 1,
|
|
||||||
metadata: { accountID: "account" },
|
|
||||||
})
|
|
||||||
const updated: Array<{ id: Credential.ID; value: Credential.Value }> = []
|
|
||||||
const refreshLayer = Connector.locationLayer.pipe(
|
|
||||||
Layer.provide(EventV2.defaultLayer),
|
|
||||||
Layer.provide(
|
|
||||||
Layer.mock(Credential.Service)({
|
|
||||||
get: () =>
|
|
||||||
Effect.succeed(
|
|
||||||
new Credential.Info({
|
|
||||||
id: credentialID,
|
|
||||||
connectorID,
|
|
||||||
methodID,
|
|
||||||
label: "Personal",
|
|
||||||
value: current,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
update: (id, input) =>
|
|
||||||
Effect.sync(() => {
|
|
||||||
if (input.value) updated.push({ id, value: input.value })
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const connectors = yield* Connector.Service
|
|
||||||
yield* connectors.update((editor) =>
|
|
||||||
editor.method.update({
|
|
||||||
connectorID,
|
|
||||||
method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }),
|
|
||||||
authorize: () => Effect.die("unexpected authorization"),
|
|
||||||
refresh: (value) =>
|
|
||||||
Effect.succeed(
|
|
||||||
new Credential.OAuth({
|
|
||||||
type: "oauth",
|
|
||||||
access: "new-access",
|
|
||||||
refresh: "new-refresh",
|
|
||||||
expires: 2,
|
|
||||||
metadata: value.metadata,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* connectors.refresh(credentialID)
|
|
||||||
expect(updated).toEqual([
|
|
||||||
{
|
|
||||||
id: credentialID,
|
|
||||||
value: new Credential.OAuth({
|
|
||||||
type: "oauth",
|
|
||||||
access: "new-access",
|
|
||||||
refresh: "new-refresh",
|
|
||||||
expires: 2,
|
|
||||||
metadata: { accountID: "account" },
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}).pipe(Effect.provide(refreshLayer))
|
|
||||||
})
|
|
||||||
|
|
||||||
it.effect("completes code OAuth once and stores the credential", () => {
|
|
||||||
const created: Array<{
|
|
||||||
connectorID: Connector.ID
|
|
||||||
methodID: Connector.MethodID
|
|
||||||
label?: string
|
|
||||||
value: Credential.Value
|
|
||||||
}> = []
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const connectors = yield* Connector.Service
|
|
||||||
const connectorID = Connector.ID.make("openai")
|
|
||||||
const methodID = Connector.MethodID.make("chatgpt")
|
|
||||||
yield* connectors.update((editor) =>
|
|
||||||
editor.method.update({
|
|
||||||
connectorID,
|
|
||||||
method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }),
|
|
||||||
authorize: () =>
|
|
||||||
Effect.succeed({
|
|
||||||
mode: "code" as const,
|
|
||||||
url: "https://example.com/authorize",
|
|
||||||
instructions: "Paste the code",
|
|
||||||
callback: (code: string) =>
|
|
||||||
Effect.succeed(
|
|
||||||
new Credential.OAuth({
|
|
||||||
type: "oauth",
|
|
||||||
access: "access",
|
|
||||||
refresh: "refresh",
|
|
||||||
expires: 1,
|
|
||||||
metadata: { code },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {}, label: "Personal" })
|
|
||||||
expect(attempt.mode).toBe("code")
|
|
||||||
yield* connectors.connect.oauth.complete({ attemptID: attempt.attemptID, code: "1234" })
|
|
||||||
|
|
||||||
expect(created[0]).toEqual({
|
|
||||||
connectorID,
|
|
||||||
methodID,
|
|
||||||
label: "Personal",
|
|
||||||
value: new Credential.OAuth({
|
|
||||||
type: "oauth",
|
|
||||||
access: "access",
|
|
||||||
refresh: "refresh",
|
|
||||||
expires: 1,
|
|
||||||
metadata: { code: "1234" },
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}).pipe(Effect.provide(connectionLayer(created)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it.effect("keeps code attempts open when the code is missing and closes them on cancel", () => {
|
|
||||||
const created: Array<{
|
|
||||||
connectorID: Connector.ID
|
|
||||||
methodID: Connector.MethodID
|
|
||||||
label?: string
|
|
||||||
value: Credential.Value
|
|
||||||
}> = []
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const connectors = yield* Connector.Service
|
|
||||||
const connectorID = Connector.ID.make("openai")
|
|
||||||
const methodID = Connector.MethodID.make("chatgpt")
|
|
||||||
let closed = false
|
|
||||||
yield* connectors.update((editor) =>
|
|
||||||
editor.method.update({
|
|
||||||
connectorID,
|
|
||||||
method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }),
|
|
||||||
authorize: () =>
|
|
||||||
Effect.addFinalizer(() => Effect.sync(() => (closed = true))).pipe(
|
|
||||||
Effect.as({
|
|
||||||
mode: "code" as const,
|
|
||||||
url: "https://example.com/authorize",
|
|
||||||
instructions: "Paste the code",
|
|
||||||
callback: () => Effect.die("unexpected callback"),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {} })
|
|
||||||
expect(
|
|
||||||
yield* connectors.connect.oauth.complete({ attemptID: attempt.attemptID }).pipe(Effect.flip),
|
|
||||||
).toBeInstanceOf(Connector.CodeRequiredError)
|
|
||||||
expect(closed).toBe(false)
|
|
||||||
yield* connectors.connect.oauth.cancel(attempt.attemptID)
|
|
||||||
expect(closed).toBe(true)
|
|
||||||
expect(created).toEqual([])
|
|
||||||
}).pipe(Effect.provide(connectionLayer(created)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it.effect("completes auto OAuth in the background", () => {
|
|
||||||
const created: Array<{
|
|
||||||
connectorID: Connector.ID
|
|
||||||
methodID: Connector.MethodID
|
|
||||||
label?: string
|
|
||||||
value: Credential.Value
|
|
||||||
}> = []
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const connectors = yield* Connector.Service
|
|
||||||
const connectorID = Connector.ID.make("openai")
|
|
||||||
const methodID = Connector.MethodID.make("browser")
|
|
||||||
yield* connectors.update((editor) =>
|
|
||||||
editor.method.update({
|
|
||||||
connectorID,
|
|
||||||
method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "Browser" }),
|
|
||||||
authorize: () =>
|
|
||||||
Effect.succeed({
|
|
||||||
mode: "auto" as const,
|
|
||||||
url: "https://example.com/authorize",
|
|
||||||
instructions: "Sign in",
|
|
||||||
callback: Effect.succeed(
|
|
||||||
new Credential.OAuth({ type: "oauth", access: "access", refresh: "refresh", expires: 1 }),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {} })
|
|
||||||
yield* Effect.yieldNow
|
|
||||||
expect(yield* connectors.connect.oauth.status(attempt.attemptID)).toEqual({
|
|
||||||
status: "complete",
|
|
||||||
time: attempt.time,
|
|
||||||
})
|
|
||||||
expect(created).toHaveLength(1)
|
|
||||||
}).pipe(Effect.provide(connectionLayer(created)))
|
|
||||||
})
|
|
||||||
|
|
||||||
it.effect("expires abandoned OAuth attempts", () => {
|
|
||||||
const created: Array<{
|
|
||||||
connectorID: Connector.ID
|
|
||||||
methodID: Connector.MethodID
|
|
||||||
label?: string
|
|
||||||
value: Credential.Value
|
|
||||||
}> = []
|
|
||||||
return Effect.gen(function* () {
|
|
||||||
const connectors = yield* Connector.Service
|
|
||||||
const connectorID = Connector.ID.make("openai")
|
|
||||||
const methodID = Connector.MethodID.make("browser")
|
|
||||||
let closed = false
|
|
||||||
yield* connectors.update((editor) =>
|
|
||||||
editor.method.update({
|
|
||||||
connectorID,
|
|
||||||
method: new Connector.OAuthMethod({ id: methodID, type: "oauth", label: "Browser" }),
|
|
||||||
authorize: () =>
|
|
||||||
Effect.addFinalizer(() => Effect.sync(() => (closed = true))).pipe(
|
|
||||||
Effect.as({
|
|
||||||
mode: "auto" as const,
|
|
||||||
url: "https://example.com/authorize",
|
|
||||||
instructions: "Sign in",
|
|
||||||
callback: Effect.never,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const attempt = yield* connectors.connect.oauth.begin({ connectorID, methodID, inputs: {} })
|
|
||||||
expect(attempt.time.expires - attempt.time.created).toBe(Duration.toMillis(Duration.minutes(10)))
|
|
||||||
yield* TestClock.adjust(Duration.minutes(10))
|
|
||||||
yield* Effect.yieldNow
|
|
||||||
expect(yield* connectors.connect.oauth.status(attempt.attemptID)).toEqual({
|
|
||||||
status: "expired",
|
|
||||||
time: attempt.time,
|
|
||||||
})
|
|
||||||
expect(closed).toBe(true)
|
|
||||||
expect(created).toEqual([])
|
|
||||||
}).pipe(Effect.provide(connectionLayer(created)))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,101 +1,20 @@
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Fiber, Layer, Stream } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
import { Connector } from "@opencode-ai/core/connector"
|
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { FSUtil } from "@opencode-ai/core/fs-util"
|
|
||||||
import { Global } from "@opencode-ai/core/global"
|
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
import { testEffect } from "./lib/effect"
|
import { it } from "./lib/effect"
|
||||||
|
|
||||||
const it = testEffect(PluginV2.locationLayer.pipe(Layer.provide(EventV2.defaultLayer)))
|
function layer(directory: string) {
|
||||||
|
|
||||||
function testLayer(directory: string) {
|
|
||||||
return Credential.layer.pipe(
|
return Credential.layer.pipe(
|
||||||
Layer.fresh,
|
|
||||||
Layer.provide(Database.layerFromPath(path.join(directory, "credential.db")).pipe(Layer.fresh)),
|
Layer.provide(Database.layerFromPath(path.join(directory, "credential.db")).pipe(Layer.fresh)),
|
||||||
Layer.provideMerge(EventV2.defaultLayer),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Credential", () => {
|
describe("Credential", () => {
|
||||||
it.live("imports supported legacy auth.json credentials once", () =>
|
it.live("stores, updates, lists, and removes credentials", () =>
|
||||||
Effect.acquireRelease(
|
|
||||||
Effect.promise(() => tmpdir()),
|
|
||||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
||||||
).pipe(
|
|
||||||
Effect.flatMap((tmp) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
yield* Effect.promise(() =>
|
|
||||||
Bun.write(
|
|
||||||
path.join(tmp.path, "auth.json"),
|
|
||||||
JSON.stringify({
|
|
||||||
openai: {
|
|
||||||
type: "oauth",
|
|
||||||
refresh: "refresh",
|
|
||||||
access: "access",
|
|
||||||
expires: 123,
|
|
||||||
accountId: "account",
|
|
||||||
},
|
|
||||||
azure: { type: "api", key: "key", metadata: { resourceName: "resource" } },
|
|
||||||
ignored: { type: "wellknown", key: "TOKEN", token: "secret" },
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
const database = Database.layerFromPath(path.join(tmp.path, "credential.db")).pipe(Layer.fresh)
|
|
||||||
const global = Global.layerWith({ data: tmp.path })
|
|
||||||
const importer = Credential.legacyImportLayer.pipe(
|
|
||||||
Layer.provide(database),
|
|
||||||
Layer.provide(FSUtil.defaultLayer),
|
|
||||||
Layer.provide(global),
|
|
||||||
)
|
|
||||||
const credentials = Credential.layer.pipe(
|
|
||||||
Layer.provide(database),
|
|
||||||
Layer.provide(EventV2.defaultLayer),
|
|
||||||
Layer.provideMerge(importer),
|
|
||||||
)
|
|
||||||
const result = yield* Effect.gen(function* () {
|
|
||||||
const service = yield* Credential.Service
|
|
||||||
return yield* service.all()
|
|
||||||
}).pipe(Effect.provide(credentials), Effect.scoped)
|
|
||||||
|
|
||||||
expect(result).toHaveLength(2)
|
|
||||||
expect(result).toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
connectorID: Connector.ID.make("openai"),
|
|
||||||
methodID: Connector.MethodID.make("chatgpt-browser"),
|
|
||||||
label: "Imported",
|
|
||||||
value: expect.objectContaining({
|
|
||||||
type: "oauth",
|
|
||||||
refresh: "refresh",
|
|
||||||
access: "access",
|
|
||||||
expires: 123,
|
|
||||||
metadata: { accountID: "account" },
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
expect(result).toContainEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
connectorID: Connector.ID.make("azure"),
|
|
||||||
methodID: Connector.MethodID.make("api-key"),
|
|
||||||
value: expect.objectContaining({ type: "key", key: "key", metadata: { resourceName: "resource" } }),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
yield* importer.pipe(Layer.build, Effect.scoped)
|
|
||||||
const after = yield* Effect.gen(function* () {
|
|
||||||
return yield* (yield* Credential.Service).all()
|
|
||||||
}).pipe(Effect.provide(credentials), Effect.scoped)
|
|
||||||
expect(after).toHaveLength(2)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.live("emits credential lifecycle events", () =>
|
|
||||||
Effect.acquireRelease(
|
Effect.acquireRelease(
|
||||||
Effect.promise(() => tmpdir()),
|
Effect.promise(() => tmpdir()),
|
||||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
||||||
@@ -103,103 +22,27 @@ describe("Credential", () => {
|
|||||||
Effect.flatMap((tmp) =>
|
Effect.flatMap((tmp) =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const credentials = yield* Credential.Service
|
const credentials = yield* Credential.Service
|
||||||
const eventSvc = yield* EventV2.Service
|
const integrationID = Integration.ID.make("openai")
|
||||||
const addedFiber = yield* eventSvc
|
const created = yield* credentials.create({
|
||||||
.subscribe(Credential.Event.Added)
|
integrationID,
|
||||||
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
|
label: "Work",
|
||||||
const switchedFiber = yield* eventSvc
|
value: new Credential.Key({ type: "key", key: "secret" }),
|
||||||
.subscribe(Credential.Event.Switched)
|
|
||||||
.pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped)
|
|
||||||
const removedFiber = yield* eventSvc
|
|
||||||
.subscribe(Credential.Event.Removed)
|
|
||||||
.pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
|
|
||||||
|
|
||||||
yield* Effect.yieldNow
|
|
||||||
|
|
||||||
const first = yield* credentials.create({
|
|
||||||
connectorID: Connector.ID.make("lifecycle"),
|
|
||||||
methodID: Connector.MethodID.make("key"),
|
|
||||||
value: new Credential.Key({ type: "key", key: "raw-key" }),
|
|
||||||
})
|
|
||||||
expect(first).toBeDefined()
|
|
||||||
if (!first) return
|
|
||||||
expect(first.label).toBe("default")
|
|
||||||
expect(first.value.type).toBe("key")
|
|
||||||
if (first.value.type === "key") expect(first.value.key).toBe("raw-key")
|
|
||||||
|
|
||||||
yield* credentials.update(first.id, { label: "keep" })
|
|
||||||
const updated = yield* credentials.get(first.id)
|
|
||||||
expect(updated?.label).toBe("keep")
|
|
||||||
expect(updated?.value.type).toBe("key")
|
|
||||||
if (updated?.value.type === "key") expect(updated.value.key).toBe("raw-key")
|
|
||||||
|
|
||||||
const second = yield* credentials.create({
|
|
||||||
connectorID: Connector.ID.make("lifecycle"),
|
|
||||||
methodID: Connector.MethodID.make("key"),
|
|
||||||
value: new Credential.Key({ type: "key", key: "second-key" }),
|
|
||||||
})
|
|
||||||
expect(second).toBeDefined()
|
|
||||||
if (!second) return
|
|
||||||
|
|
||||||
yield* credentials.remove(second.id)
|
|
||||||
const added = Array.from(yield* Fiber.join(addedFiber))
|
|
||||||
const switched = Array.from(yield* Fiber.join(switchedFiber))
|
|
||||||
const removed = Array.from(yield* Fiber.join(removedFiber))
|
|
||||||
expect(added.map((event) => event.data.credential.id)).toEqual([first.id, second.id])
|
|
||||||
expect(switched.map((event) => event.data)).toEqual([
|
|
||||||
{ connectorID: Connector.ID.make("lifecycle"), from: undefined, to: first.id },
|
|
||||||
{ connectorID: Connector.ID.make("lifecycle"), from: first.id, to: second.id },
|
|
||||||
{ connectorID: Connector.ID.make("lifecycle"), from: second.id, to: first.id },
|
|
||||||
])
|
|
||||||
expect(removed[0]?.data.credential.id).toBe(second.id)
|
|
||||||
}).pipe(Effect.provide(testLayer(tmp.path))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.live("always switches to newly created credentials", () =>
|
|
||||||
Effect.acquireRelease(
|
|
||||||
Effect.promise(() => tmpdir()),
|
|
||||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
||||||
).pipe(
|
|
||||||
Effect.flatMap((tmp) =>
|
|
||||||
Effect.gen(function* () {
|
|
||||||
const credentials = yield* Credential.Service
|
|
||||||
const eventSvc = yield* EventV2.Service
|
|
||||||
const switchedFiber = yield* eventSvc
|
|
||||||
.subscribe(Credential.Event.Switched)
|
|
||||||
.pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped)
|
|
||||||
|
|
||||||
yield* Effect.yieldNow
|
|
||||||
|
|
||||||
const first = yield* credentials.create({
|
|
||||||
connectorID: Connector.ID.make("switch"),
|
|
||||||
methodID: Connector.MethodID.make("key"),
|
|
||||||
value: new Credential.Key({ type: "key", key: "first-key" }),
|
|
||||||
})
|
|
||||||
const second = yield* credentials.create({
|
|
||||||
connectorID: Connector.ID.make("switch"),
|
|
||||||
methodID: Connector.MethodID.make("key"),
|
|
||||||
value: new Credential.Key({ type: "key", key: "second-key" }),
|
|
||||||
})
|
|
||||||
const third = yield* credentials.create({
|
|
||||||
connectorID: Connector.ID.make("switch"),
|
|
||||||
methodID: Connector.MethodID.make("key"),
|
|
||||||
value: new Credential.Key({ type: "key", key: "third-key" }),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(first).toBeDefined()
|
expect(yield* credentials.list(integrationID)).toEqual([created])
|
||||||
expect(second).toBeDefined()
|
yield* credentials.update(created.id, { label: "Personal" })
|
||||||
expect(third).toBeDefined()
|
expect((yield* credentials.list(integrationID))[0]?.label).toBe("Personal")
|
||||||
if (!first || !second || !third) return
|
|
||||||
|
|
||||||
expect((yield* credentials.active(Connector.ID.make("switch")))?.id).toBe(third.id)
|
const replacement = yield* credentials.create({
|
||||||
expect(Array.from(yield* Fiber.join(switchedFiber)).map((event) => event.data)).toEqual([
|
integrationID,
|
||||||
{ connectorID: Connector.ID.make("switch"), from: undefined, to: first.id },
|
label: "Replacement",
|
||||||
{ connectorID: Connector.ID.make("switch"), from: first.id, to: second.id },
|
value: new Credential.Key({ type: "key", key: "replacement" }),
|
||||||
{ connectorID: Connector.ID.make("switch"), from: second.id, to: third.id },
|
})
|
||||||
])
|
expect(yield* credentials.list(integrationID)).toEqual([replacement])
|
||||||
}).pipe(Effect.provide(testLayer(tmp.path))),
|
|
||||||
|
yield* credentials.remove(replacement.id)
|
||||||
|
expect(yield* credentials.list(integrationID)).toEqual([])
|
||||||
|
}).pipe(Effect.provide(layer(tmp.path))),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -92,6 +92,18 @@ describe("DatabaseMigration", () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("rejects a non-empty database without a session table", async () => {
|
||||||
|
await expect(
|
||||||
|
run(
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const db = yield* makeDb
|
||||||
|
yield* db.run(sql`CREATE TABLE unrelated (id text PRIMARY KEY)`)
|
||||||
|
yield* DatabaseMigration.apply(db)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).rejects.toThrow("Database is not empty and has no session table")
|
||||||
|
})
|
||||||
|
|
||||||
test("backfills existing Context Epoch rows to the build agent", async () => {
|
test("backfills existing Context Epoch rows to the build agent", async () => {
|
||||||
await run(
|
await run(
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|||||||
@@ -0,0 +1,401 @@
|
|||||||
|
import { describe, expect } from "bun:test"
|
||||||
|
import { Duration, Effect, Exit, Layer, Scope } from "effect"
|
||||||
|
import * as TestClock from "effect/testing/TestClock"
|
||||||
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
|
import { IntegrationConnection } from "@opencode-ai/core/integration/connection"
|
||||||
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
|
import { it } from "./lib/effect"
|
||||||
|
|
||||||
|
const layer = Integration.locationLayer.pipe(
|
||||||
|
Layer.provide(EventV2.defaultLayer),
|
||||||
|
Layer.provide(
|
||||||
|
Layer.mock(Credential.Service)({
|
||||||
|
create: () => Effect.die("unexpected credential creation"),
|
||||||
|
list: () => Effect.succeed([]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
function connectionLayer(
|
||||||
|
created: Array<{
|
||||||
|
integrationID: Integration.ID
|
||||||
|
label?: string
|
||||||
|
value: Credential.Info
|
||||||
|
}>,
|
||||||
|
) {
|
||||||
|
return Integration.locationLayer.pipe(
|
||||||
|
Layer.provide(EventV2.defaultLayer),
|
||||||
|
Layer.provide(
|
||||||
|
Layer.mock(Credential.Service)({
|
||||||
|
create: (input) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
created.push(input)
|
||||||
|
return new Credential.Stored({
|
||||||
|
id: Credential.ID.create(),
|
||||||
|
integrationID: input.integrationID,
|
||||||
|
label: input.label ?? "default",
|
||||||
|
value: input.value,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
list: () => Effect.succeed([]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Integration", () => {
|
||||||
|
it.effect("registers integrations through the editor", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
|
const scope = yield* Scope.fork(yield* Scope.Scope)
|
||||||
|
const openai = Integration.ID.make("openai")
|
||||||
|
|
||||||
|
yield* integrations
|
||||||
|
.update((editor) => editor.update(openai, (integration) => (integration.name = "OpenAI")))
|
||||||
|
.pipe(Scope.provide(scope))
|
||||||
|
expect(yield* integrations.get(openai)).toEqual(
|
||||||
|
new Integration.Info({ id: openai, name: "OpenAI", methods: [], connections: [] }),
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* Scope.close(scope, Exit.void)
|
||||||
|
expect(yield* integrations.get(openai)).toBeUndefined()
|
||||||
|
}).pipe(Effect.provide(layer)),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("reveals the previous registration when an override closes", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
|
const id = Integration.ID.make("openai")
|
||||||
|
const first = yield* Scope.fork(yield* Scope.Scope)
|
||||||
|
const second = yield* Scope.fork(yield* Scope.Scope)
|
||||||
|
|
||||||
|
yield* integrations
|
||||||
|
.update((editor) => editor.update(id, (integration) => (integration.name = "OpenAI")))
|
||||||
|
.pipe(Scope.provide(first))
|
||||||
|
yield* integrations
|
||||||
|
.update((editor) => editor.update(id, (integration) => (integration.name = "OpenAI Override")))
|
||||||
|
.pipe(Scope.provide(second))
|
||||||
|
expect((yield* integrations.get(id))?.name).toBe("OpenAI Override")
|
||||||
|
|
||||||
|
yield* Scope.close(second, Exit.void)
|
||||||
|
expect((yield* integrations.get(id))?.name).toBe("OpenAI")
|
||||||
|
expect((yield* integrations.list()).map((integration) => integration.id)).toEqual([id])
|
||||||
|
}).pipe(Effect.provide(layer)),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("registers and overrides methods independently", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
|
const integrationID = Integration.ID.make("openai")
|
||||||
|
const methodID = Integration.MethodID.make("chatgpt")
|
||||||
|
const first = yield* Scope.fork(yield* Scope.Scope)
|
||||||
|
const second = yield* Scope.fork(yield* Scope.Scope)
|
||||||
|
const authorize = () =>
|
||||||
|
Effect.succeed({
|
||||||
|
mode: "auto" as const,
|
||||||
|
url: "https://example.com/authorize",
|
||||||
|
instructions: "Sign in",
|
||||||
|
callback: Effect.never,
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* integrations
|
||||||
|
.update((editor) =>
|
||||||
|
editor.method.update({
|
||||||
|
integrationID,
|
||||||
|
method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }),
|
||||||
|
authorize,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.pipe(Scope.provide(first))
|
||||||
|
yield* integrations
|
||||||
|
.update((editor) => {
|
||||||
|
expect(editor.get(integrationID)).toEqual({ id: integrationID, name: "openai" })
|
||||||
|
expect(editor.list()).toEqual([{ id: integrationID, name: "openai" }])
|
||||||
|
expect(editor.method.list(integrationID)).toEqual([
|
||||||
|
expect.objectContaining({ id: methodID, label: "ChatGPT" }),
|
||||||
|
])
|
||||||
|
editor.method.update({
|
||||||
|
integrationID,
|
||||||
|
method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT Override" }),
|
||||||
|
authorize,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.pipe(Scope.provide(second))
|
||||||
|
|
||||||
|
expect((yield* integrations.get(integrationID))?.name).toBe("openai")
|
||||||
|
expect((yield* integrations.get(integrationID))?.methods[0]).toMatchObject({ label: "ChatGPT Override" })
|
||||||
|
|
||||||
|
yield* Scope.close(second, Exit.void)
|
||||||
|
expect((yield* integrations.get(integrationID))?.methods[0]).toMatchObject({ label: "ChatGPT" })
|
||||||
|
expect((yield* integrations.get(integrationID))?.methods).toEqual([expect.objectContaining({ id: methodID })])
|
||||||
|
}).pipe(Effect.provide(layer)),
|
||||||
|
)
|
||||||
|
|
||||||
|
it.effect("connects with a key and stores the credential", () => {
|
||||||
|
const created: Array<{
|
||||||
|
integrationID: Integration.ID
|
||||||
|
label?: string
|
||||||
|
value: Credential.Info
|
||||||
|
}> = []
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
|
const integrationID = Integration.ID.make("openai")
|
||||||
|
yield* integrations.update((editor) =>
|
||||||
|
editor.method.update({
|
||||||
|
integrationID,
|
||||||
|
method: new Integration.KeyMethod({ type: "key", label: "API key" }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
yield* integrations.connect.key({
|
||||||
|
integrationID,
|
||||||
|
key: "secret",
|
||||||
|
label: "Work",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(created).toEqual([
|
||||||
|
{
|
||||||
|
integrationID,
|
||||||
|
label: "Work",
|
||||||
|
value: new Credential.Key({ type: "key", key: "secret" }),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}).pipe(Effect.provide(connectionLayer(created)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it.effect("completes code OAuth once and stores the credential", () => {
|
||||||
|
const created: Array<{
|
||||||
|
integrationID: Integration.ID
|
||||||
|
label?: string
|
||||||
|
value: Credential.Info
|
||||||
|
}> = []
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
|
const integrationID = Integration.ID.make("openai")
|
||||||
|
const methodID = Integration.MethodID.make("chatgpt")
|
||||||
|
yield* integrations.update((editor) =>
|
||||||
|
editor.method.update({
|
||||||
|
integrationID,
|
||||||
|
method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }),
|
||||||
|
authorize: () =>
|
||||||
|
Effect.succeed({
|
||||||
|
mode: "code" as const,
|
||||||
|
url: "https://example.com/authorize",
|
||||||
|
instructions: "Paste the code",
|
||||||
|
callback: (code: string) =>
|
||||||
|
Effect.succeed(
|
||||||
|
new Credential.OAuth({
|
||||||
|
type: "oauth",
|
||||||
|
methodID,
|
||||||
|
access: "access",
|
||||||
|
refresh: "refresh",
|
||||||
|
expires: 1,
|
||||||
|
metadata: { code },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const attempt = yield* integrations.connect.oauth({
|
||||||
|
integrationID,
|
||||||
|
methodID,
|
||||||
|
inputs: {},
|
||||||
|
label: "Personal",
|
||||||
|
})
|
||||||
|
expect(attempt.mode).toBe("code")
|
||||||
|
yield* integrations.attempt.complete({ attemptID: attempt.attemptID, code: "1234" })
|
||||||
|
|
||||||
|
expect(created[0]).toEqual({
|
||||||
|
integrationID,
|
||||||
|
label: "Personal",
|
||||||
|
value: new Credential.OAuth({
|
||||||
|
type: "oauth",
|
||||||
|
methodID,
|
||||||
|
access: "access",
|
||||||
|
refresh: "refresh",
|
||||||
|
expires: 1,
|
||||||
|
metadata: { code: "1234" },
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}).pipe(Effect.provide(connectionLayer(created)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it.effect("keeps code attempts open when the code is missing and closes them on cancel", () => {
|
||||||
|
const created: Array<{
|
||||||
|
integrationID: Integration.ID
|
||||||
|
label?: string
|
||||||
|
value: Credential.Info
|
||||||
|
}> = []
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
|
const integrationID = Integration.ID.make("openai")
|
||||||
|
const methodID = Integration.MethodID.make("chatgpt")
|
||||||
|
let closed = false
|
||||||
|
yield* integrations.update((editor) =>
|
||||||
|
editor.method.update({
|
||||||
|
integrationID,
|
||||||
|
method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "ChatGPT" }),
|
||||||
|
authorize: () =>
|
||||||
|
Effect.addFinalizer(() => Effect.sync(() => (closed = true))).pipe(
|
||||||
|
Effect.as({
|
||||||
|
mode: "code" as const,
|
||||||
|
url: "https://example.com/authorize",
|
||||||
|
instructions: "Paste the code",
|
||||||
|
callback: () => Effect.die("unexpected callback"),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const attempt = yield* integrations.connect.oauth({ integrationID, methodID, inputs: {} })
|
||||||
|
expect(yield* integrations.attempt.complete({ attemptID: attempt.attemptID }).pipe(Effect.flip)).toBeInstanceOf(
|
||||||
|
Integration.CodeRequiredError,
|
||||||
|
)
|
||||||
|
expect(closed).toBe(false)
|
||||||
|
yield* integrations.attempt.cancel(attempt.attemptID)
|
||||||
|
expect(closed).toBe(true)
|
||||||
|
expect(created).toEqual([])
|
||||||
|
}).pipe(Effect.provide(connectionLayer(created)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it.effect("completes auto OAuth in the background", () => {
|
||||||
|
const created: Array<{
|
||||||
|
integrationID: Integration.ID
|
||||||
|
label?: string
|
||||||
|
value: Credential.Info
|
||||||
|
}> = []
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
|
const integrationID = Integration.ID.make("openai")
|
||||||
|
const methodID = Integration.MethodID.make("browser")
|
||||||
|
yield* integrations.update((editor) =>
|
||||||
|
editor.method.update({
|
||||||
|
integrationID,
|
||||||
|
method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "Browser" }),
|
||||||
|
authorize: () =>
|
||||||
|
Effect.succeed({
|
||||||
|
mode: "auto" as const,
|
||||||
|
url: "https://example.com/authorize",
|
||||||
|
instructions: "Sign in",
|
||||||
|
callback: Effect.succeed(
|
||||||
|
new Credential.OAuth({ type: "oauth", methodID, access: "access", refresh: "refresh", expires: 1 }),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const attempt = yield* integrations.connect.oauth({ integrationID, methodID, inputs: {} })
|
||||||
|
yield* Effect.yieldNow
|
||||||
|
expect(yield* integrations.attempt.status(attempt.attemptID)).toEqual({
|
||||||
|
status: "complete",
|
||||||
|
time: attempt.time,
|
||||||
|
})
|
||||||
|
expect(created).toHaveLength(1)
|
||||||
|
}).pipe(Effect.provide(connectionLayer(created)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it.effect("expires abandoned OAuth attempts", () => {
|
||||||
|
const created: Array<{
|
||||||
|
integrationID: Integration.ID
|
||||||
|
label?: string
|
||||||
|
value: Credential.Info
|
||||||
|
}> = []
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
|
const integrationID = Integration.ID.make("openai")
|
||||||
|
const methodID = Integration.MethodID.make("browser")
|
||||||
|
let closed = false
|
||||||
|
yield* integrations.update((editor) =>
|
||||||
|
editor.method.update({
|
||||||
|
integrationID,
|
||||||
|
method: new Integration.OAuthMethod({ id: methodID, type: "oauth", label: "Browser" }),
|
||||||
|
authorize: () =>
|
||||||
|
Effect.addFinalizer(() => Effect.sync(() => (closed = true))).pipe(
|
||||||
|
Effect.as({
|
||||||
|
mode: "auto" as const,
|
||||||
|
url: "https://example.com/authorize",
|
||||||
|
instructions: "Sign in",
|
||||||
|
callback: Effect.never,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const attempt = yield* integrations.connect.oauth({ integrationID, methodID, inputs: {} })
|
||||||
|
expect(attempt.time.expires - attempt.time.created).toBe(Duration.toMillis(Duration.minutes(10)))
|
||||||
|
yield* TestClock.adjust(Duration.minutes(10))
|
||||||
|
yield* Effect.yieldNow
|
||||||
|
expect(yield* integrations.attempt.status(attempt.attemptID)).toEqual({
|
||||||
|
status: "expired",
|
||||||
|
time: attempt.time,
|
||||||
|
})
|
||||||
|
expect(closed).toBe(true)
|
||||||
|
expect(created).toEqual([])
|
||||||
|
}).pipe(Effect.provide(connectionLayer(created)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it.effect("projects credential and env connections", () => {
|
||||||
|
const integrationID = Integration.ID.make("acme")
|
||||||
|
const rows = [
|
||||||
|
{
|
||||||
|
id: Credential.ID.create(),
|
||||||
|
integrationID,
|
||||||
|
label: "Work",
|
||||||
|
value: new Credential.Key({ type: "key", key: "a" }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: Credential.ID.create(),
|
||||||
|
integrationID,
|
||||||
|
label: "Personal",
|
||||||
|
value: new Credential.Key({ type: "key", key: "b" }),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
const projectionLayer = Integration.locationLayer.pipe(
|
||||||
|
Layer.provide(EventV2.defaultLayer),
|
||||||
|
Layer.provide(
|
||||||
|
Layer.mock(Credential.Service)({
|
||||||
|
list: () => Effect.succeed(rows.map((row) => new Credential.Stored(row))),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return Effect.acquireUseRelease(
|
||||||
|
Effect.sync(() => {
|
||||||
|
const previous = process.env.INTEGRATION_TEST_ACME_KEY
|
||||||
|
process.env.INTEGRATION_TEST_ACME_KEY = "secret"
|
||||||
|
delete process.env.INTEGRATION_TEST_ACME_MISSING
|
||||||
|
return previous
|
||||||
|
}),
|
||||||
|
() =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const integrations = yield* Integration.Service
|
||||||
|
yield* integrations.update((editor) =>
|
||||||
|
editor.method.update({
|
||||||
|
integrationID,
|
||||||
|
method: new Integration.EnvMethod({
|
||||||
|
type: "env",
|
||||||
|
names: ["INTEGRATION_TEST_ACME_KEY", "INTEGRATION_TEST_ACME_MISSING"],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Stored credentials and detected env vars appear as connections.
|
||||||
|
expect((yield* integrations.get(integrationID))?.connections).toEqual([
|
||||||
|
new IntegrationConnection.CredentialInfo({ type: "credential", id: rows[0]!.id, label: "Work" }),
|
||||||
|
new IntegrationConnection.CredentialInfo({
|
||||||
|
type: "credential",
|
||||||
|
id: rows[1]!.id,
|
||||||
|
label: "Personal",
|
||||||
|
}),
|
||||||
|
new IntegrationConnection.EnvInfo({ type: "env", name: "INTEGRATION_TEST_ACME_KEY" }),
|
||||||
|
])
|
||||||
|
}).pipe(Effect.provide(projectionLayer)),
|
||||||
|
(previous) =>
|
||||||
|
Effect.sync(() => {
|
||||||
|
if (previous === undefined) delete process.env.INTEGRATION_TEST_ACME_KEY
|
||||||
|
else process.env.INTEGRATION_TEST_ACME_KEY = previous
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -34,9 +34,9 @@ const it = testEffect(
|
|||||||
Layer.mergeAll(
|
Layer.mergeAll(
|
||||||
Project.defaultLayer,
|
Project.defaultLayer,
|
||||||
EventV2.defaultLayer,
|
EventV2.defaultLayer,
|
||||||
|
Credential.defaultLayer,
|
||||||
Credential.layer.pipe(
|
Credential.layer.pipe(
|
||||||
Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh)),
|
Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh)),
|
||||||
Layer.provide(EventV2.defaultLayer),
|
|
||||||
),
|
),
|
||||||
Npm.defaultLayer,
|
Npm.defaultLayer,
|
||||||
ModelsDev.defaultLayer,
|
ModelsDev.defaultLayer,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from "path"
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Connector } from "@opencode-ai/core/connector"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
@@ -23,14 +23,25 @@ const locationLayer = Layer.succeed(
|
|||||||
)
|
)
|
||||||
const plugins = PluginV2.layer.pipe(Layer.provide(events))
|
const plugins = PluginV2.layer.pipe(Layer.provide(events))
|
||||||
const policy = Policy.layer.pipe(Layer.provide(locationLayer))
|
const policy = Policy.layer.pipe(Layer.provide(locationLayer))
|
||||||
const credentials = Credential.layer.pipe(Layer.provide(Database.layerFromPath(":memory:")), Layer.provide(events))
|
const connections = Credential.layer.pipe(
|
||||||
const catalog = Catalog.layer.pipe(Layer.provide(Layer.mergeAll(events, locationLayer, plugins, policy, credentials)))
|
Layer.fresh,
|
||||||
const connectors = Connector.locationLayer.pipe(Layer.provide(credentials), Layer.provide(events))
|
Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh)),
|
||||||
const layer = Layer.mergeAll(catalog, connectors, credentials, events, locationLayer, plugins)
|
Layer.provide(events),
|
||||||
|
)
|
||||||
|
const catalog = Catalog.layer.pipe(Layer.provide(Layer.mergeAll(events, locationLayer, plugins, policy, connections)))
|
||||||
|
const integrations = Integration.locationLayer.pipe(Layer.provide(events), Layer.provide(connections))
|
||||||
|
const layer = Layer.mergeAll(
|
||||||
|
catalog.pipe(Layer.provide(connections)),
|
||||||
|
integrations,
|
||||||
|
connections,
|
||||||
|
events,
|
||||||
|
locationLayer,
|
||||||
|
plugins,
|
||||||
|
)
|
||||||
const it = testEffect(layer)
|
const it = testEffect(layer)
|
||||||
|
|
||||||
describe("ModelsDevPlugin", () => {
|
describe("ModelsDevPlugin", () => {
|
||||||
it.effect("registers key connectors for providers with environment variables", () =>
|
it.effect("registers key methods for providers with environment variables", () =>
|
||||||
Effect.acquireUseRelease(
|
Effect.acquireUseRelease(
|
||||||
Effect.sync(() => {
|
Effect.sync(() => {
|
||||||
const previous = {
|
const previous = {
|
||||||
@@ -44,14 +55,19 @@ describe("ModelsDevPlugin", () => {
|
|||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
yield* ModelsDevPlugin.effect
|
yield* ModelsDevPlugin.effect
|
||||||
const connectors = yield* Connector.Service
|
const integrations = yield* Integration.Service
|
||||||
expect(yield* connectors.list()).toEqual([
|
expect(yield* integrations.list()).toEqual([
|
||||||
new Connector.Info({
|
new Integration.Info({
|
||||||
id: Connector.ID.make("acme"),
|
id: Integration.ID.make("acme"),
|
||||||
name: "Acme",
|
name: "Acme",
|
||||||
methods: [
|
methods: [
|
||||||
new Connector.KeyMethod({ id: Connector.MethodID.make("api-key"), type: "key", label: "API Key" }),
|
new Integration.KeyMethod({ type: "key" }),
|
||||||
|
new Integration.EnvMethod({
|
||||||
|
type: "env",
|
||||||
|
names: ["ACME_API_KEY"],
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
|
connections: [],
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
}).pipe(Effect.provide(ModelsDev.defaultLayer)),
|
}).pipe(Effect.provide(ModelsDev.defaultLayer)),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
import { Connector } from "@opencode-ai/core/connector"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
@@ -14,14 +14,15 @@ import { location } from "../fixture/location"
|
|||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper"
|
import { fakeSelectorSdk, it, model, npmLayer, provider, withEnv } from "./provider-helper"
|
||||||
|
|
||||||
|
const database = Database.layerFromPath(":memory:").pipe(Layer.fresh)
|
||||||
|
const preferences = Credential.layer.pipe(Layer.provide(database))
|
||||||
|
const accounts = Layer.merge(
|
||||||
|
Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)),
|
||||||
|
preferences,
|
||||||
|
)
|
||||||
const itWithAccount = testEffect(
|
const itWithAccount = testEffect(
|
||||||
Catalog.locationLayer.pipe(
|
Catalog.locationLayer.pipe(
|
||||||
Layer.provideMerge(
|
Layer.provideMerge(accounts),
|
||||||
Credential.layer.pipe(
|
|
||||||
Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh)),
|
|
||||||
Layer.provide(EventV2.defaultLayer),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Layer.provideMerge(EventV2.defaultLayer),
|
Layer.provideMerge(EventV2.defaultLayer),
|
||||||
Layer.provideMerge(
|
Layer.provideMerge(
|
||||||
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
|
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
|
||||||
@@ -83,8 +84,7 @@ describe("AzurePlugin", () => {
|
|||||||
const credentials = yield* Credential.Service
|
const credentials = yield* Credential.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* credentials.create({
|
yield* credentials.create({
|
||||||
connectorID: Connector.ID.make("azure"),
|
integrationID: Integration.ID.make("azure"),
|
||||||
methodID: Connector.MethodID.make("api-key"),
|
|
||||||
value: new Credential.Key({
|
value: new Credential.Key({
|
||||||
type: "key",
|
type: "key",
|
||||||
key: "key",
|
key: "key",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
import { Connector } from "@opencode-ai/core/connector"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
@@ -15,14 +15,15 @@ import { location } from "../fixture/location"
|
|||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { fakeSelectorSdk, it, model, npmLayer, withEnv } from "./provider-helper"
|
import { fakeSelectorSdk, it, model, npmLayer, withEnv } from "./provider-helper"
|
||||||
|
|
||||||
|
const database = Database.layerFromPath(":memory:").pipe(Layer.fresh)
|
||||||
|
const preferences = Credential.layer.pipe(Layer.provide(database))
|
||||||
|
const accounts = Layer.merge(
|
||||||
|
Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)),
|
||||||
|
preferences,
|
||||||
|
)
|
||||||
const itWithAccount = testEffect(
|
const itWithAccount = testEffect(
|
||||||
Catalog.locationLayer.pipe(
|
Catalog.locationLayer.pipe(
|
||||||
Layer.provideMerge(
|
Layer.provideMerge(accounts),
|
||||||
Credential.layer.pipe(
|
|
||||||
Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh)),
|
|
||||||
Layer.provide(EventV2.defaultLayer),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Layer.provideMerge(EventV2.defaultLayer),
|
Layer.provideMerge(EventV2.defaultLayer),
|
||||||
Layer.provideMerge(
|
Layer.provideMerge(
|
||||||
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
|
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("test") }))),
|
||||||
@@ -137,8 +138,7 @@ describe("CloudflareWorkersAIPlugin", () => {
|
|||||||
const credentials = yield* Credential.Service
|
const credentials = yield* Credential.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* credentials.create({
|
yield* credentials.create({
|
||||||
connectorID: Connector.ID.make("cloudflare-workers-ai"),
|
integrationID: Integration.ID.make("cloudflare-workers-ai"),
|
||||||
methodID: Connector.MethodID.make("api-key"),
|
|
||||||
value: new Credential.Key({
|
value: new Credential.Key({
|
||||||
type: "key",
|
type: "key",
|
||||||
key: "account-key",
|
key: "account-key",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, mock } from "bun:test"
|
import { describe, expect, mock } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
import { Connector } from "@opencode-ai/core/connector"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { Database } from "@opencode-ai/core/database/database"
|
import { Database } from "@opencode-ai/core/database/database"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
@@ -15,6 +15,12 @@ import { testEffect } from "../lib/effect"
|
|||||||
import { it, model, npmLayer, withEnv } from "./provider-helper"
|
import { it, model, npmLayer, withEnv } from "./provider-helper"
|
||||||
|
|
||||||
const gitlabSDKOptions: Record<string, unknown>[] = []
|
const gitlabSDKOptions: Record<string, unknown>[] = []
|
||||||
|
const database = Database.layerFromPath(":memory:").pipe(Layer.fresh)
|
||||||
|
const preferences = Credential.layer.pipe(Layer.provide(database))
|
||||||
|
const accounts = Layer.merge(
|
||||||
|
Credential.layer.pipe(Layer.provide(database), Layer.provide(preferences), Layer.provide(EventV2.defaultLayer)),
|
||||||
|
preferences,
|
||||||
|
)
|
||||||
|
|
||||||
void mock.module("gitlab-ai-provider", () => ({
|
void mock.module("gitlab-ai-provider", () => ({
|
||||||
VERSION: "test-version",
|
VERSION: "test-version",
|
||||||
@@ -31,12 +37,7 @@ void mock.module("gitlab-ai-provider", () => ({
|
|||||||
|
|
||||||
const itWithAccount = testEffect(
|
const itWithAccount = testEffect(
|
||||||
Catalog.locationLayer.pipe(
|
Catalog.locationLayer.pipe(
|
||||||
Layer.provideMerge(
|
Layer.provideMerge(accounts),
|
||||||
Credential.layer.pipe(
|
|
||||||
Layer.provide(Database.layerFromPath(":memory:").pipe(Layer.fresh)),
|
|
||||||
Layer.provide(EventV2.defaultLayer),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Layer.provideMerge(EventV2.defaultLayer),
|
Layer.provideMerge(EventV2.defaultLayer),
|
||||||
Layer.provideMerge(
|
Layer.provideMerge(
|
||||||
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/") }))),
|
Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/") }))),
|
||||||
@@ -174,8 +175,7 @@ describe("GitLabPlugin", () => {
|
|||||||
const credentials = yield* Credential.Service
|
const credentials = yield* Credential.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* credentials.create({
|
yield* credentials.create({
|
||||||
connectorID: Connector.ID.make("gitlab"),
|
integrationID: Integration.ID.make("gitlab"),
|
||||||
methodID: Connector.MethodID.make("api-key"),
|
|
||||||
value: new Credential.Key({ type: "key", key: "account-token" }),
|
value: new Credential.Key({ type: "key", key: "account-token" }),
|
||||||
})
|
})
|
||||||
yield* plugin.add(GitLabPlugin)
|
yield* plugin.add(GitLabPlugin)
|
||||||
@@ -208,10 +208,10 @@ describe("GitLabPlugin", () => {
|
|||||||
const credentials = yield* Credential.Service
|
const credentials = yield* Credential.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* credentials.create({
|
yield* credentials.create({
|
||||||
connectorID: Connector.ID.make("gitlab"),
|
integrationID: Integration.ID.make("gitlab"),
|
||||||
methodID: Connector.MethodID.make("oauth"),
|
|
||||||
value: new Credential.OAuth({
|
value: new Credential.OAuth({
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
|
methodID: Integration.MethodID.make("oauth"),
|
||||||
refresh: "refresh-token",
|
refresh: "refresh-token",
|
||||||
access: "account-oauth-token",
|
access: "account-oauth-token",
|
||||||
expires: 9999999999999,
|
expires: 9999999999999,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { LanguageModelV3 } from "@ai-sdk/provider"
|
|||||||
import { expect } from "bun:test"
|
import { expect } from "bun:test"
|
||||||
import { Effect, Layer, Option } from "effect"
|
import { Effect, Layer, Option } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Connector } from "@opencode-ai/core/connector"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { Credential } from "@opencode-ai/core/credential"
|
import { Credential } from "@opencode-ai/core/credential"
|
||||||
import { EventV2 } from "@opencode-ai/core/event"
|
import { EventV2 } from "@opencode-ai/core/event"
|
||||||
import { Location } from "@opencode-ai/core/location"
|
import { Location } from "@opencode-ai/core/location"
|
||||||
@@ -48,15 +48,24 @@ export const catalogLayer = Layer.succeed(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
const connectors = Connector.locationLayer.pipe(
|
const integrations = Integration.locationLayer.pipe(
|
||||||
Layer.provide(EventV2.defaultLayer),
|
Layer.provide(EventV2.defaultLayer),
|
||||||
Layer.provide(Layer.mock(Credential.Service)({ create: () => Effect.die("unexpected credential creation") })),
|
Layer.provide(
|
||||||
|
Layer.mock(Credential.Service)({
|
||||||
|
create: () => Effect.die("unexpected credential creation"),
|
||||||
|
list: () => Effect.succeed([]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const it = testEffect(
|
export const it = testEffect(
|
||||||
Catalog.locationLayer.pipe(
|
Catalog.locationLayer.pipe(
|
||||||
Layer.provideMerge(connectors),
|
Layer.provideMerge(integrations),
|
||||||
Layer.provideMerge(Layer.mock(Credential.Service)({ activeAll: () => Effect.succeed(new Map()) })),
|
Layer.provideMerge(
|
||||||
|
Layer.mock(Credential.Service)({
|
||||||
|
all: () => Effect.succeed([]),
|
||||||
|
}),
|
||||||
|
),
|
||||||
Layer.provideMerge(EventV2.defaultLayer),
|
Layer.provideMerge(EventV2.defaultLayer),
|
||||||
Layer.provideMerge(locationLayer),
|
Layer.provideMerge(locationLayer),
|
||||||
Layer.provideMerge(npmLayer),
|
Layer.provideMerge(npmLayer),
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Catalog } from "@opencode-ai/core/catalog"
|
import { Catalog } from "@opencode-ai/core/catalog"
|
||||||
import { Connector } from "@opencode-ai/core/connector"
|
import { Integration } from "@opencode-ai/core/integration"
|
||||||
import { ModelV2 } from "@opencode-ai/core/model"
|
import { ModelV2 } from "@opencode-ai/core/model"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
|
import { OpenAIPlugin } from "@opencode-ai/core/plugin/provider/openai"
|
||||||
import { ProviderV2 } from "@opencode-ai/core/provider"
|
import { ProviderV2 } from "@opencode-ai/core/provider"
|
||||||
import { fakeSelectorSdk, it, model, provider } from "./provider-helper"
|
import { fakeSelectorSdk, it, model, provider } from "./provider-helper"
|
||||||
|
|
||||||
function add(plugin: PluginV2.Interface, connectors: Connector.Interface) {
|
function add(plugin: PluginV2.Interface, integrations: Integration.Interface) {
|
||||||
return plugin.add({
|
return plugin.add({
|
||||||
...OpenAIPlugin,
|
...OpenAIPlugin,
|
||||||
effect: OpenAIPlugin.effect.pipe(Effect.provideService(Connector.Service, connectors)),
|
effect: OpenAIPlugin.effect.pipe(Effect.provideService(Integration.Service, integrations)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,15 +19,15 @@ describe("OpenAIPlugin", () => {
|
|||||||
it.effect("registers browser and headless ChatGPT OAuth methods", () =>
|
it.effect("registers browser and headless ChatGPT OAuth methods", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* add(plugin, yield* Connector.Service)
|
yield* add(plugin, yield* Integration.Service)
|
||||||
expect((yield* (yield* Connector.Service).get(Connector.ID.make("openai")))?.methods).toEqual([
|
expect((yield* (yield* Integration.Service).get(Integration.ID.make("openai")))?.methods).toEqual([
|
||||||
new Connector.OAuthMethod({
|
new Integration.OAuthMethod({
|
||||||
id: Connector.MethodID.make("chatgpt-browser"),
|
id: Integration.MethodID.make("chatgpt-browser"),
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
label: "ChatGPT Pro/Plus (browser)",
|
label: "ChatGPT Pro/Plus (browser)",
|
||||||
}),
|
}),
|
||||||
new Connector.OAuthMethod({
|
new Integration.OAuthMethod({
|
||||||
id: Connector.MethodID.make("chatgpt-headless"),
|
id: Integration.MethodID.make("chatgpt-headless"),
|
||||||
type: "oauth",
|
type: "oauth",
|
||||||
label: "ChatGPT Pro/Plus (headless)",
|
label: "ChatGPT Pro/Plus (headless)",
|
||||||
}),
|
}),
|
||||||
@@ -38,7 +38,7 @@ describe("OpenAIPlugin", () => {
|
|||||||
it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () =>
|
it.effect("creates an OpenAI SDK for @ai-sdk/openai using the provider ID as SDK name", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* add(plugin, yield* Connector.Service)
|
yield* add(plugin, yield* Integration.Service)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{
|
{
|
||||||
@@ -55,7 +55,7 @@ describe("OpenAIPlugin", () => {
|
|||||||
it.effect("ignores non-OpenAI SDK packages", () =>
|
it.effect("ignores non-OpenAI SDK packages", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
yield* add(plugin, yield* Connector.Service)
|
yield* add(plugin, yield* Integration.Service)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.sdk",
|
"aisdk.sdk",
|
||||||
{ model: model("openai", "gpt-5"), package: "@ai-sdk/openai-compatible", options: { name: "openai" } },
|
{ model: model("openai", "gpt-5"), package: "@ai-sdk/openai-compatible", options: { name: "openai" } },
|
||||||
@@ -69,7 +69,7 @@ describe("OpenAIPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* add(plugin, yield* Connector.Service)
|
yield* add(plugin, yield* Integration.Service)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{
|
{
|
||||||
@@ -90,7 +90,7 @@ describe("OpenAIPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const calls: string[] = []
|
const calls: string[] = []
|
||||||
yield* add(plugin, yield* Connector.Service)
|
yield* add(plugin, yield* Integration.Service)
|
||||||
const result = yield* plugin.trigger(
|
const result = yield* plugin.trigger(
|
||||||
"aisdk.language",
|
"aisdk.language",
|
||||||
{ model: model("anthropic", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },
|
{ model: model("anthropic", "gpt-5"), sdk: fakeSelectorSdk(calls), options: {} },
|
||||||
@@ -105,7 +105,7 @@ describe("OpenAIPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* add(plugin, yield* Connector.Service)
|
yield* add(plugin, yield* Integration.Service)
|
||||||
const transform = yield* catalog.transform()
|
const transform = yield* catalog.transform()
|
||||||
yield* transform((catalog) => {
|
yield* transform((catalog) => {
|
||||||
const item = provider("openai", { api: { type: "aisdk", package: "@ai-sdk/openai" } })
|
const item = provider("openai", { api: { type: "aisdk", package: "@ai-sdk/openai" } })
|
||||||
@@ -124,7 +124,7 @@ describe("OpenAIPlugin", () => {
|
|||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const plugin = yield* PluginV2.Service
|
const plugin = yield* PluginV2.Service
|
||||||
const catalog = yield* Catalog.Service
|
const catalog = yield* Catalog.Service
|
||||||
yield* add(plugin, yield* Connector.Service)
|
yield* add(plugin, yield* Integration.Service)
|
||||||
const transform = yield* catalog.transform()
|
const transform = yield* catalog.transform()
|
||||||
yield* transform((catalog) => {
|
yield* transform((catalog) => {
|
||||||
const item = provider("custom-openai")
|
const item = provider("custom-openai")
|
||||||
|
|||||||
@@ -650,49 +650,67 @@ const scenarios: Scenario[] = [
|
|||||||
http.protected.get("/api/agent", "v2.agent.list").json(200, locationData(array)),
|
http.protected.get("/api/agent", "v2.agent.list").json(200, locationData(array)),
|
||||||
http.protected.get("/api/model", "v2.model.list").json(200, locationData(array)),
|
http.protected.get("/api/model", "v2.model.list").json(200, locationData(array)),
|
||||||
http.protected.get("/api/provider", "v2.provider.list").json(200, locationData(array)),
|
http.protected.get("/api/provider", "v2.provider.list").json(200, locationData(array)),
|
||||||
http.protected.get("/api/connector", "v2.connector.list").json(200, locationData(array)),
|
http.protected.get("/api/integration", "v2.integration.list").json(200, locationData(array)),
|
||||||
http.protected
|
http.protected
|
||||||
.get("/api/connector/{connectorID}", "v2.connector.get")
|
.get("/api/integration/{integrationID}", "v2.integration.get")
|
||||||
.at((ctx) => ({ path: route("/api/connector/{connectorID}", { connectorID: "missing" }), headers: ctx.headers() }))
|
.at((ctx) => ({
|
||||||
|
path: route("/api/integration/{integrationID}", { integrationID: "missing" }),
|
||||||
|
headers: ctx.headers(),
|
||||||
|
}))
|
||||||
.json(200, object),
|
.json(200, object),
|
||||||
http.protected
|
http.protected
|
||||||
.post("/api/connector/{connectorID}/connect/key", "v2.connector.connect.key")
|
.post("/api/integration/{integrationID}/connect/key", "v2.integration.connect.key")
|
||||||
.at((ctx) => ({
|
.at((ctx) => ({
|
||||||
path: route("/api/connector/{connectorID}/connect/key", { connectorID: "missing" }),
|
path: route("/api/integration/{integrationID}/connect/key", { integrationID: "missing" }),
|
||||||
headers: ctx.headers(),
|
headers: ctx.headers(),
|
||||||
body: { methodID: "missing", key: "test", inputs: {} },
|
body: { key: "test" },
|
||||||
}))
|
}))
|
||||||
.status(500, undefined, "status"),
|
.status(500, undefined, "status"),
|
||||||
http.protected
|
http.protected
|
||||||
.post("/api/connector/{connectorID}/connect/oauth", "v2.connector.connect.oauth.begin")
|
.post("/api/integration/{integrationID}/connect/oauth", "v2.integration.connect.oauth")
|
||||||
.at((ctx) => ({
|
.at((ctx) => ({
|
||||||
path: route("/api/connector/{connectorID}/connect/oauth", { connectorID: "missing" }),
|
path: route("/api/integration/{integrationID}/connect/oauth", { integrationID: "missing" }),
|
||||||
headers: ctx.headers(),
|
headers: ctx.headers(),
|
||||||
body: { methodID: "missing", inputs: {} },
|
body: { methodID: "missing", inputs: {} },
|
||||||
}))
|
}))
|
||||||
.status(500, undefined, "status"),
|
.status(500, undefined, "status"),
|
||||||
http.protected
|
http.protected
|
||||||
.get("/api/connector/oauth/{attemptID}", "v2.connector.connect.oauth.status")
|
.get("/api/integration/attempt/{attemptID}", "v2.integration.attempt.status")
|
||||||
.at((ctx) => ({
|
.at((ctx) => ({
|
||||||
path: route("/api/connector/oauth/{attemptID}", { attemptID: "con_missing" }),
|
path: route("/api/integration/attempt/{attemptID}", { attemptID: "con_missing" }),
|
||||||
headers: ctx.headers(),
|
headers: ctx.headers(),
|
||||||
}))
|
}))
|
||||||
.status(500, undefined, "status"),
|
.status(500, undefined, "status"),
|
||||||
http.protected
|
http.protected
|
||||||
.post("/api/connector/oauth/{attemptID}/complete", "v2.connector.connect.oauth.complete")
|
.post("/api/integration/attempt/{attemptID}/complete", "v2.integration.attempt.complete")
|
||||||
.at((ctx) => ({
|
.at((ctx) => ({
|
||||||
path: route("/api/connector/oauth/{attemptID}/complete", { attemptID: "con_missing" }),
|
path: route("/api/integration/attempt/{attemptID}/complete", { attemptID: "con_missing" }),
|
||||||
headers: ctx.headers(),
|
headers: ctx.headers(),
|
||||||
body: {},
|
body: {},
|
||||||
}))
|
}))
|
||||||
.status(500, undefined, "status"),
|
.status(500, undefined, "status"),
|
||||||
http.protected
|
http.protected
|
||||||
.delete("/api/connector/oauth/{attemptID}", "v2.connector.connect.oauth.cancel")
|
.delete("/api/integration/attempt/{attemptID}", "v2.integration.attempt.cancel")
|
||||||
.at((ctx) => ({
|
.at((ctx) => ({
|
||||||
path: route("/api/connector/oauth/{attemptID}", { attemptID: "con_missing" }),
|
path: route("/api/integration/attempt/{attemptID}", { attemptID: "con_missing" }),
|
||||||
headers: ctx.headers(),
|
headers: ctx.headers(),
|
||||||
}))
|
}))
|
||||||
.status(204, undefined, "status"),
|
.status(204, undefined, "status"),
|
||||||
|
http.protected
|
||||||
|
.delete("/api/credential/{credentialID}", "v2.credential.remove")
|
||||||
|
.at((ctx) => ({
|
||||||
|
path: route("/api/credential/{credentialID}", { credentialID: "cred_missing" }),
|
||||||
|
headers: ctx.headers(),
|
||||||
|
}))
|
||||||
|
.status(204, undefined, "status"),
|
||||||
|
http.protected
|
||||||
|
.patch("/api/credential/{credentialID}", "v2.credential.update")
|
||||||
|
.at((ctx) => ({
|
||||||
|
path: route("/api/credential/{credentialID}", { credentialID: "cred_missing" }),
|
||||||
|
headers: ctx.headers(),
|
||||||
|
body: { label: "Work" },
|
||||||
|
}))
|
||||||
|
.status(204, undefined, "status"),
|
||||||
http.protected.get("/api/command", "v2.command.list").json(200, locationData(array)),
|
http.protected.get("/api/command", "v2.command.list").json(200, locationData(array)),
|
||||||
http.protected.get("/api/skill", "v2.skill.list").json(200, locationData(array)),
|
http.protected.get("/api/skill", "v2.skill.list").json(200, locationData(array)),
|
||||||
http.protected
|
http.protected
|
||||||
|
|||||||
@@ -115,25 +115,27 @@ describe("PublicApi OpenAPI v2 errors", () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("documents connector discovery and connection routes", () => {
|
test("documents integration discovery and connection routes", () => {
|
||||||
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
|
const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
|
||||||
|
|
||||||
for (const [method, path] of [
|
for (const [method, path] of [
|
||||||
["get", "/api/connector"],
|
["get", "/api/integration"],
|
||||||
["get", "/api/connector/{connectorID}"],
|
["get", "/api/integration/{integrationID}"],
|
||||||
["post", "/api/connector/{connectorID}/connect/key"],
|
["post", "/api/integration/{integrationID}/connect/key"],
|
||||||
["post", "/api/connector/{connectorID}/connect/oauth"],
|
["post", "/api/integration/{integrationID}/connect/oauth"],
|
||||||
["get", "/api/connector/oauth/{attemptID}"],
|
["get", "/api/integration/attempt/{attemptID}"],
|
||||||
["post", "/api/connector/oauth/{attemptID}/complete"],
|
["post", "/api/integration/attempt/{attemptID}/complete"],
|
||||||
["delete", "/api/connector/oauth/{attemptID}"],
|
["delete", "/api/integration/attempt/{attemptID}"],
|
||||||
|
["delete", "/api/credential/{credentialID}"],
|
||||||
|
["patch", "/api/credential/{credentialID}"],
|
||||||
] as const) {
|
] as const) {
|
||||||
expect(spec.paths[path]?.[method], `${method.toUpperCase()} ${path}`).toBeDefined()
|
expect(spec.paths[path]?.[method], `${method.toUpperCase()} ${path}`).toBeDefined()
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const path of [
|
for (const path of [
|
||||||
"/api/connector/{connectorID}/connect/key",
|
"/api/integration/{integrationID}/connect/key",
|
||||||
"/api/connector/{connectorID}/connect/oauth",
|
"/api/integration/{integrationID}/connect/oauth",
|
||||||
"/api/connector/oauth/{attemptID}/complete",
|
"/api/integration/attempt/{attemptID}/complete",
|
||||||
]) {
|
]) {
|
||||||
expect(spec.paths[path]?.post?.requestBody?.required, path).toBe(true)
|
expect(spec.paths[path]?.post?.requestBody?.required, path).toBe(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
|
||||||
import { Database } from "bun:sqlite"
|
|
||||||
import { drizzle } from "drizzle-orm/bun-sqlite"
|
|
||||||
import { migrate } from "drizzle-orm/bun-sqlite/migrator"
|
|
||||||
import { existsSync, readFileSync, readdirSync } from "fs"
|
|
||||||
import path from "path"
|
|
||||||
|
|
||||||
const target = "20260507164347_add_workspace_time"
|
|
||||||
|
|
||||||
function migrations() {
|
|
||||||
return readdirSync(path.join(import.meta.dirname, "../../../core/migration"), { withFileTypes: true })
|
|
||||||
.filter(
|
|
||||||
(entry) =>
|
|
||||||
entry.isDirectory() &&
|
|
||||||
existsSync(path.join(import.meta.dirname, "../../../core/migration", entry.name, "migration.sql")),
|
|
||||||
)
|
|
||||||
.map((entry) => ({
|
|
||||||
name: entry.name,
|
|
||||||
timestamp: Number(entry.name.split("_")[0]),
|
|
||||||
sql: readFileSync(
|
|
||||||
path.join(import.meta.dirname, "../../../core/migration", entry.name, "migration.sql"),
|
|
||||||
"utf-8",
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.timestamp - b.timestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("workspace time migration", () => {
|
|
||||||
test("discards existing workspace rows during the beta reset", () => {
|
|
||||||
const sqlite = new Database(":memory:")
|
|
||||||
const db = drizzle({ client: sqlite })
|
|
||||||
const entries = migrations()
|
|
||||||
const index = entries.findIndex((entry) => entry.name === target)
|
|
||||||
|
|
||||||
expect(index).toBeGreaterThan(0)
|
|
||||||
|
|
||||||
migrate(db, entries.slice(0, index))
|
|
||||||
sqlite.run(
|
|
||||||
"INSERT INTO project (id, worktree, vcs, name, time_created, time_updated, sandboxes) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
||||||
["project_1", "/tmp/project", "git", "project", 1, 1, "[]"],
|
|
||||||
)
|
|
||||||
sqlite.run(
|
|
||||||
"INSERT INTO workspace (id, type, name, branch, directory, extra, project_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
||||||
["workspace_1", "local", "main", "main", "/tmp/project", null, "project_1"],
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(() => migrate(db, entries.slice(index))).not.toThrow()
|
|
||||||
expect(sqlite.query("SELECT time_used FROM workspace WHERE id = ?").get("workspace_1")).toBeNull()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user