feat(database): add model_providers and role_model_routes tables
Adds workspace-scoped provider registry schema, relations, migration 0010, and seedDefaultRoleRoutes() for pipeline defaults when aliases exist. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
fde698f439
commit
f04122fd04
12 changed files with 3597 additions and 8 deletions
36
packages/database/migrations/0010_legal_stryfe.sql
Normal file
36
packages/database/migrations/0010_legal_stryfe.sql
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
CREATE TABLE "model_providers" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"workspace_id" uuid NOT NULL,
|
||||||
|
"alias" varchar(120) NOT NULL,
|
||||||
|
"display_name" varchar(200) NOT NULL,
|
||||||
|
"provider_type" varchar(40) NOT NULL,
|
||||||
|
"model_id" varchar(200) NOT NULL,
|
||||||
|
"base_url" text,
|
||||||
|
"api_key_env_var" varchar(120),
|
||||||
|
"extra_headers" jsonb,
|
||||||
|
"default_params" jsonb,
|
||||||
|
"metadata" jsonb,
|
||||||
|
"enabled" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "role_model_routes" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"workspace_id" uuid NOT NULL,
|
||||||
|
"role_id" varchar(120) NOT NULL,
|
||||||
|
"provider_alias_id" uuid NOT NULL,
|
||||||
|
"enabled" boolean DEFAULT true NOT NULL,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "model_providers" ADD CONSTRAINT "model_providers_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "role_model_routes" ADD CONSTRAINT "role_model_routes_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspaces"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "role_model_routes" ADD CONSTRAINT "role_model_routes_provider_alias_id_model_providers_id_fk" FOREIGN KEY ("provider_alias_id") REFERENCES "public"."model_providers"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX "model_providers_workspace_id_idx" ON "model_providers" USING btree ("workspace_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "model_providers_workspace_id_alias_unique" ON "model_providers" USING btree ("workspace_id","alias");--> statement-breakpoint
|
||||||
|
CREATE INDEX "role_model_routes_workspace_id_idx" ON "role_model_routes" USING btree ("workspace_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "role_model_routes_workspace_id_role_id_unique" ON "role_model_routes" USING btree ("workspace_id","role_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "role_model_routes_provider_alias_id_idx" ON "role_model_routes" USING btree ("provider_alias_id");
|
||||||
3351
packages/database/migrations/meta/0010_snapshot.json
Normal file
3351
packages/database/migrations/meta/0010_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -71,6 +71,13 @@
|
||||||
"when": 1780455865684,
|
"when": 1780455865684,
|
||||||
"tag": "0009_loving_rogue",
|
"tag": "0009_loving_rogue",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1783450794221,
|
||||||
|
"tag": "0010_legal_stryfe",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -9,7 +9,8 @@
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
"./schema": "./src/schema/index.ts",
|
"./schema": "./src/schema/index.ts",
|
||||||
"./client": "./src/client.ts",
|
"./client": "./src/client.ts",
|
||||||
"./markdown-backlog": "./src/markdown-backlog/index.ts"
|
"./markdown-backlog": "./src/markdown-backlog/index.ts",
|
||||||
|
"./model-routing": "./src/model-routing/index.ts"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
|
|
@ -23,6 +24,7 @@
|
||||||
"import:markdown-backlog": "tsx src/scripts/import-markdown-backlog.ts"
|
"import:markdown-backlog": "tsx src/scripts/import-markdown-backlog.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tasks/shared": "workspace:*",
|
||||||
"drizzle-orm": "^0.38.0",
|
"drizzle-orm": "^0.38.0",
|
||||||
"postgres": "^3.4.5",
|
"postgres": "^3.4.5",
|
||||||
"yaml": "^2.8.3",
|
"yaml": "^2.8.3",
|
||||||
|
|
|
||||||
2
packages/database/src/model-routing/index.ts
Normal file
2
packages/database/src/model-routing/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { seedDefaultRoleRoutes } from "./seed-default-role-routes";
|
||||||
|
export type { SeedDefaultRoleRoutesResult } from "./seed-default-role-routes";
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
||||||
|
|
||||||
|
import { PIPELINE_DEFAULT_ROLE_ROUTES } from "@tasks/shared/model-routing";
|
||||||
|
|
||||||
|
import type * as schema from "../schema/index";
|
||||||
|
import { modelProviders, roleModelRoutes } from "../schema/index";
|
||||||
|
|
||||||
|
type Database = PostgresJsDatabase<typeof schema>;
|
||||||
|
|
||||||
|
export type SeedDefaultRoleRoutesResult = {
|
||||||
|
inserted: number;
|
||||||
|
skipped: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts pipeline default role → alias routes for a workspace when matching
|
||||||
|
* provider aliases already exist. Does not create providers.
|
||||||
|
*/
|
||||||
|
export async function seedDefaultRoleRoutes(
|
||||||
|
db: Database,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<SeedDefaultRoleRoutesResult> {
|
||||||
|
const providers = await db
|
||||||
|
.select({ id: modelProviders.id, alias: modelProviders.alias })
|
||||||
|
.from(modelProviders)
|
||||||
|
.where(eq(modelProviders.workspaceId, workspaceId));
|
||||||
|
|
||||||
|
const aliasToId = new Map(providers.map((row) => [row.alias, row.id]));
|
||||||
|
let inserted = 0;
|
||||||
|
let skipped = 0;
|
||||||
|
let sortOrder = 0;
|
||||||
|
|
||||||
|
for (const [roleId, alias] of Object.entries(PIPELINE_DEFAULT_ROLE_ROUTES)) {
|
||||||
|
const providerId = aliasToId.get(alias);
|
||||||
|
if (!providerId) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [existing] = await db
|
||||||
|
.select({ id: roleModelRoutes.id })
|
||||||
|
.from(roleModelRoutes)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roleModelRoutes.workspaceId, workspaceId),
|
||||||
|
eq(roleModelRoutes.roleId, roleId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(roleModelRoutes).values({
|
||||||
|
workspaceId,
|
||||||
|
roleId,
|
||||||
|
providerAliasId: providerId,
|
||||||
|
sortOrder: sortOrder++,
|
||||||
|
});
|
||||||
|
inserted++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { inserted, skipped };
|
||||||
|
}
|
||||||
|
|
@ -13,3 +13,5 @@ export * from "./markdown_backlog";
|
||||||
export * from "./cursor_sync";
|
export * from "./cursor_sync";
|
||||||
export * from "./audit";
|
export * from "./audit";
|
||||||
export * from "./agent_runs";
|
export * from "./agent_runs";
|
||||||
|
export * from "./model_providers";
|
||||||
|
export * from "./role_model_routes";
|
||||||
|
|
|
||||||
51
packages/database/src/schema/model_providers.ts
Normal file
51
packages/database/src/schema/model_providers.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import {
|
||||||
|
boolean,
|
||||||
|
index,
|
||||||
|
jsonb,
|
||||||
|
pgTable,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
uniqueIndex,
|
||||||
|
uuid,
|
||||||
|
varchar,
|
||||||
|
integer,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
import { workspaces } from "./workspaces";
|
||||||
|
|
||||||
|
/** Workspace-scoped model provider alias — secrets live in env vars, not here. */
|
||||||
|
export const modelProviders = pgTable(
|
||||||
|
"model_providers",
|
||||||
|
{
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
workspaceId: uuid("workspace_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => workspaces.id, { onDelete: "cascade" }),
|
||||||
|
alias: varchar("alias", { length: 120 }).notNull(),
|
||||||
|
displayName: varchar("display_name", { length: 200 }).notNull(),
|
||||||
|
providerType: varchar("provider_type", { length: 40 }).notNull(),
|
||||||
|
modelId: varchar("model_id", { length: 200 }).notNull(),
|
||||||
|
baseUrl: text("base_url"),
|
||||||
|
apiKeyEnvVar: varchar("api_key_env_var", { length: 120 }),
|
||||||
|
extraHeaders: jsonb("extra_headers").$type<Record<string, string> | null>(),
|
||||||
|
defaultParams: jsonb("default_params").$type<{
|
||||||
|
temperature?: number;
|
||||||
|
maxTokens?: number;
|
||||||
|
} | null>(),
|
||||||
|
metadata: jsonb("metadata").$type<Record<string, unknown> | null>(),
|
||||||
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
workspaceIdIdx: index("model_providers_workspace_id_idx").on(table.workspaceId),
|
||||||
|
workspaceAliasUnique: uniqueIndex("model_providers_workspace_id_alias_unique").on(
|
||||||
|
table.workspaceId,
|
||||||
|
table.alias,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
@ -19,6 +19,8 @@ import { cursorSyncMappings } from "./cursor_sync";
|
||||||
import { workspaces, workspaceInvites } from "./workspaces";
|
import { workspaces, workspaceInvites } from "./workspaces";
|
||||||
import { auditLog } from "./audit";
|
import { auditLog } from "./audit";
|
||||||
import { agentRuns } from "./agent_runs";
|
import { agentRuns } from "./agent_runs";
|
||||||
|
import { modelProviders } from "./model_providers";
|
||||||
|
import { roleModelRoutes } from "./role_model_routes";
|
||||||
|
|
||||||
export const objectRelations = pgTable(
|
export const objectRelations = pgTable(
|
||||||
"object_relations",
|
"object_relations",
|
||||||
|
|
@ -116,6 +118,8 @@ export const workspacesRelations = relations(workspaces, ({ one, many }) => ({
|
||||||
objectTypeDefs: many(objectTypeDefs),
|
objectTypeDefs: many(objectTypeDefs),
|
||||||
propertyDefinitions: many(propertyDefinitions),
|
propertyDefinitions: many(propertyDefinitions),
|
||||||
markdownBacklogItems: many(markdownBacklogItems),
|
markdownBacklogItems: many(markdownBacklogItems),
|
||||||
|
modelProviders: many(modelProviders),
|
||||||
|
roleModelRoutes: many(roleModelRoutes),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const objectsRelations = relations(objects, ({ one, many }) => ({
|
export const objectsRelations = relations(objects, ({ one, many }) => ({
|
||||||
|
|
@ -264,3 +268,22 @@ export const cursorSyncMappingsRelations = relations(cursorSyncMappings, ({ one
|
||||||
references: [markdownBacklogItems.id],
|
references: [markdownBacklogItems.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const modelProvidersRelations = relations(modelProviders, ({ one, many }) => ({
|
||||||
|
workspace: one(workspaces, {
|
||||||
|
fields: [modelProviders.workspaceId],
|
||||||
|
references: [workspaces.id],
|
||||||
|
}),
|
||||||
|
roleRoutes: many(roleModelRoutes),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const roleModelRoutesRelations = relations(roleModelRoutes, ({ one }) => ({
|
||||||
|
workspace: one(workspaces, {
|
||||||
|
fields: [roleModelRoutes.workspaceId],
|
||||||
|
references: [workspaces.id],
|
||||||
|
}),
|
||||||
|
provider: one(modelProviders, {
|
||||||
|
fields: [roleModelRoutes.providerAliasId],
|
||||||
|
references: [modelProviders.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
|
||||||
45
packages/database/src/schema/role_model_routes.ts
Normal file
45
packages/database/src/schema/role_model_routes.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import {
|
||||||
|
boolean,
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
pgTable,
|
||||||
|
timestamp,
|
||||||
|
uniqueIndex,
|
||||||
|
uuid,
|
||||||
|
varchar,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
import { workspaces } from "./workspaces";
|
||||||
|
import { modelProviders } from "./model_providers";
|
||||||
|
|
||||||
|
/** Maps pipeline role_id strings to a workspace provider alias row. */
|
||||||
|
export const roleModelRoutes = pgTable(
|
||||||
|
"role_model_routes",
|
||||||
|
{
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
workspaceId: uuid("workspace_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => workspaces.id, { onDelete: "cascade" }),
|
||||||
|
roleId: varchar("role_id", { length: 120 }).notNull(),
|
||||||
|
providerAliasId: uuid("provider_alias_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => modelProviders.id, { onDelete: "cascade" }),
|
||||||
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
sortOrder: integer("sort_order").notNull().default(0),
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
workspaceIdIdx: index("role_model_routes_workspace_id_idx").on(table.workspaceId),
|
||||||
|
workspaceRoleUnique: uniqueIndex(
|
||||||
|
"role_model_routes_workspace_id_role_id_unique",
|
||||||
|
).on(table.workspaceId, table.roleId),
|
||||||
|
providerAliasIdIdx: index("role_model_routes_provider_alias_id_idx").on(
|
||||||
|
table.providerAliasId,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
@ -4,7 +4,7 @@ slug: db-model-providers-migration
|
||||||
title: Drizzle schema and migration for model_providers and role_model_routes
|
title: Drizzle schema and migration for model_providers and role_model_routes
|
||||||
plan_slug: automation-foundation
|
plan_slug: automation-foundation
|
||||||
epic_slug: model-provider-registry
|
epic_slug: model-provider-registry
|
||||||
status: ready
|
status: done
|
||||||
priority: P1
|
priority: P1
|
||||||
tenant_id: global
|
tenant_id: global
|
||||||
owner: unassigned
|
owner: unassigned
|
||||||
|
|
@ -39,9 +39,9 @@ Add `model_providers` and `role_model_routes` tables with workspace scoping and
|
||||||
|
|
||||||
## Subtasks
|
## Subtasks
|
||||||
|
|
||||||
- [ ] Define Drizzle tables
|
- [x] Define Drizzle tables
|
||||||
- [ ] Generate and commit migration
|
- [x] Generate and commit migration
|
||||||
- [ ] Verify `workspace_id` index on both tables
|
- [x] Verify `workspace_id` index on both tables
|
||||||
|
|
||||||
## Estimation
|
## Estimation
|
||||||
|
|
||||||
|
|
@ -49,9 +49,9 @@ S
|
||||||
|
|
||||||
## Acceptance criteria
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] Migration applies cleanly with `pnpm db:migrate`
|
- [x] Migration applies cleanly with `pnpm db:migrate`
|
||||||
- [ ] Every query example in epic doc filters by `workspace_id`
|
- [x] Every query example in epic doc filters by `workspace_id`
|
||||||
- [ ] Types align with `packages/shared` zod schemas
|
- [x] Types align with `packages/shared` zod schemas
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,9 @@ importers:
|
||||||
|
|
||||||
packages/database:
|
packages/database:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@tasks/shared':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../shared
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.38.0
|
specifier: ^0.38.0
|
||||||
version: 0.38.4(@opentelemetry/api@1.9.0)(@types/react@19.2.14)(postgres@3.4.8)(react@19.2.4)
|
version: 0.38.4(@opentelemetry/api@1.9.0)(@types/react@19.2.14)(postgres@3.4.8)(react@19.2.4)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue