diff --git a/README.md b/README.md index e42ea73..05bdfa5 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Forge is a build orchestration service that wraps different cloud build provider - **Log Management**: Centralized log collection and retrieval for all build runs - **Environment Variables**: Encrypted environment variable storage and injection - **Build Versioning**: Track and manage different versions of your workflows -- **Instance Isolation**: Multi-tenant support with instance-based resource isolation +- **Tenant Isolation**: Multi-tenant architecture for isolated projects ## Quick Start @@ -152,20 +152,20 @@ let client = createForgeClient({ ### Core API Examples -#### 1. Instance Management +#### 1. Tenant Management -Instances represent isolated tenants or projects: +Tenants represent isolated tenants or projects: ```typescript -// Create/update an instance -let instance = await client.instance.upsert({ +// Create/update an tenant +let tenant = await client.tenant.upsert({ name: 'My Project', identifier: 'my-project', }); -// Get an instance -let retrievedInstance = await client.instance.get({ - instanceId: instance.id, +// Get an tenant +let retrievedTenant = await client.tenant.get({ + tenantId: tenant.id, }); ``` @@ -176,34 +176,34 @@ Workflows define build processes: ```typescript // Create/update a workflow let workflow = await client.workflow.upsert({ - instanceId: instance.id, + tenantId: tenant.id, name: 'Build and Test', identifier: 'build-test', }); // List workflows let workflows = await client.workflow.list({ - instanceId: instance.id, + tenantId: tenant.id, limit: 10, order: 'desc', }); // Get a specific workflow let workflowDetails = await client.workflow.get({ - instanceId: instance.id, + tenantId: tenant.id, workflowId: workflow.id, }); // Update a workflow let updated = await client.workflow.update({ - instanceId: instance.id, + tenantId: tenant.id, workflowId: workflow.id, name: 'Build, Test, and Deploy', }); // Delete a workflow await client.workflow.delete({ - instanceId: instance.id, + tenantId: tenant.id, workflowId: workflow.id, }); ``` @@ -215,7 +215,7 @@ Versions allow you to define the actual build steps: ```typescript // Create a workflow version with steps let version = await client.workflowVersion.create({ - instanceId: instance.id, + tenantId: tenant.id, workflowId: workflow.id, name: 'v1.0.0', steps: [ @@ -242,14 +242,14 @@ let version = await client.workflowVersion.create({ // List versions let versions = await client.workflowVersion.list({ - instanceId: instance.id, + tenantId: tenant.id, workflowId: workflow.id, limit: 10, }); // Get version details let versionDetails = await client.workflowVersion.get({ - instanceId: instance.id, + tenantId: tenant.id, workflowId: workflow.id, }); ``` @@ -261,7 +261,7 @@ Execute workflows with environment variables and files: ```typescript // Create a workflow run let run = await client.workflowRun.create({ - instanceId: instance.id, + tenantId: tenant.id, workflowId: workflow.id, env: { NODE_ENV: 'production', @@ -287,7 +287,7 @@ console.log('Steps:', run.steps); // List workflow runs let runs = await client.workflowRun.list({ - instanceId: instance.id, + tenantId: tenant.id, workflowId: workflow.id, limit: 20, order: 'desc', @@ -295,7 +295,7 @@ let runs = await client.workflowRun.list({ // Get detailed run information let runDetails = await client.workflowRun.get({ - instanceId: instance.id, + tenantId: tenant.id, workflowId: workflow.id, workflowRunId: run.id, }); @@ -312,7 +312,7 @@ Retrieve build logs for workflow runs: ```typescript // Get all step outputs for a run let outputs = await client.workflowRun.getOutput({ - instanceId: instance.id, + tenantId: tenant.id, workflowId: workflow.id, workflowRunId: run.id, }); @@ -326,7 +326,7 @@ for (let output of outputs) { // Get output for a specific step let stepOutput = await client.workflowRun.getOutputForStep({ - instanceId: instance.id, + tenantId: tenant.id, workflowId: workflow.id, workflowRunId: run.id, workflowRunStepId: run.steps[0].id, @@ -342,7 +342,7 @@ Access build artifacts generated during workflow runs: ```typescript // List artifacts for a workflow let artifacts = await client.workflowArtifact.list({ - instanceId: instance.id, + tenantId: tenant.id, workflowId: workflow.id, limit: 10, }); @@ -355,7 +355,7 @@ for (let artifact of artifacts.items) { // Get a specific artifact let artifact = await client.workflowArtifact.get({ - instanceId: instance.id, + tenantId: tenant.id, workflowId: workflow.id, }); diff --git a/clients/typescript/package.json b/clients/typescript/package.json index 88d8602..66c0260 100644 --- a/clients/typescript/package.json +++ b/clients/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@metorial-services/forge-client", - "version": "1.0.2", + "version": "1.0.3", "publishConfig": { "access": "public" }, diff --git a/service/prisma/schema.prisma b/service/prisma/schema.prisma index d2a3add..bfe23a3 100644 --- a/service/prisma/schema.prisma +++ b/service/prisma/schema.prisma @@ -13,7 +13,7 @@ datasource db { provider = "postgresql" } -model Instance { +model Tenant { oid BigInt @id id String @unique @@ -52,8 +52,8 @@ model Workflow { identifier String name String - instanceOid BigInt - instance Instance @relation(fields: [instanceOid], references: [oid]) + tenantOid BigInt + tenant Tenant @relation(fields: [tenantOid], references: [oid]) providerOid BigInt provider Provider @relation(fields: [providerOid], references: [oid]) @@ -69,7 +69,7 @@ model Workflow { artifacts WorkflowArtifact[] runs WorkflowRun[] - @@unique([instanceOid, identifier]) + @@unique([tenantOid, identifier]) } model WorkflowVersion { diff --git a/service/src/controllers/index.ts b/service/src/controllers/index.ts index d837231..021cf0b 100644 --- a/service/src/controllers/index.ts +++ b/service/src/controllers/index.ts @@ -1,15 +1,15 @@ import { apiMux } from '@lowerdeck/api-mux'; import { createServer, rpcMux, type InferClient } from '@lowerdeck/rpc-server'; import { app } from './_app'; -import { instanceController } from './instance'; import { providerController } from './provider'; +import { tenantController } from './tenant'; import { workflowController } from './workflow'; import { workflowArtifactController } from './workflowArtifact'; import { workflowRunController } from './workflowRun'; import { workflowVersionController } from './workflowVersion'; export let rootController = app.controller({ - instance: instanceController, + tenant: tenantController, provider: providerController, workflow: workflowController, workflowArtifact: workflowArtifactController, diff --git a/service/src/controllers/instance.ts b/service/src/controllers/instance.ts deleted file mode 100644 index 8733390..0000000 --- a/service/src/controllers/instance.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { v } from '@lowerdeck/validation'; -import { instancePresenter } from '../presenters/instance'; -import { instanceService } from '../services'; -import { app } from './_app'; - -export let instanceApp = app.use(async ctx => { - let instanceId = ctx.body.instanceId; - if (!instanceId) throw new Error('Instance ID is required'); - - let instance = await instanceService.getInstanceById({ id: instanceId }); - - return { instance }; -}); - -export let instanceController = app.controller({ - upsert: app - .handler() - .input( - v.object({ - name: v.string(), - identifier: v.string() - }) - ) - .do(async ctx => { - let instance = await instanceService.upsertInstance({ - input: { - name: ctx.input.name, - identifier: ctx.input.identifier - } - }); - return instancePresenter(instance); - }), - - get: instanceApp - .handler() - .input( - v.object({ - instanceId: v.string() - }) - ) - .do(async ctx => instancePresenter(ctx.instance)) -}); diff --git a/service/src/controllers/tenant.ts b/service/src/controllers/tenant.ts new file mode 100644 index 0000000..25e2e49 --- /dev/null +++ b/service/src/controllers/tenant.ts @@ -0,0 +1,42 @@ +import { v } from '@lowerdeck/validation'; +import { tenantPresenter } from '../presenters/tenant'; +import { tenantService } from '../services'; +import { app } from './_app'; + +export let tenantApp = app.use(async ctx => { + let tenantId = ctx.body.tenantId; + if (!tenantId) throw new Error('Tenant ID is required'); + + let tenant = await tenantService.getTenantById({ id: tenantId }); + + return { tenant }; +}); + +export let tenantController = app.controller({ + upsert: app + .handler() + .input( + v.object({ + name: v.string(), + identifier: v.string() + }) + ) + .do(async ctx => { + let tenant = await tenantService.upsertTenant({ + input: { + name: ctx.input.name, + identifier: ctx.input.identifier + } + }); + return tenantPresenter(tenant); + }), + + get: tenantApp + .handler() + .input( + v.object({ + tenantId: v.string() + }) + ) + .do(async ctx => tenantPresenter(ctx.tenant)) +}); diff --git a/service/src/controllers/workflow.ts b/service/src/controllers/workflow.ts index 50434a0..4f8c1f9 100644 --- a/service/src/controllers/workflow.ts +++ b/service/src/controllers/workflow.ts @@ -3,26 +3,26 @@ import { v } from '@lowerdeck/validation'; import { workflowPresenter } from '../presenters/workflow'; import { workflowService } from '../services'; import { app } from './_app'; -import { instanceApp } from './instance'; +import { tenantApp } from './tenant'; -export let workflowApp = instanceApp.use(async ctx => { +export let workflowApp = tenantApp.use(async ctx => { let workflowId = ctx.body.workflowId; if (!workflowId) throw new Error('Workflow ID is required'); let workflow = await workflowService.getWorkflowById({ id: workflowId, - instance: ctx.instance + tenant: ctx.tenant }); return { workflow }; }); export let workflowController = app.controller({ - upsert: instanceApp + upsert: tenantApp .handler() .input( v.object({ - instanceId: v.string(), + tenantId: v.string(), name: v.string(), identifier: v.string() @@ -30,7 +30,7 @@ export let workflowController = app.controller({ ) .do(async ctx => { let workflow = await workflowService.upsertWorkflow({ - instance: ctx.instance, + tenant: ctx.tenant, input: { name: ctx.input.name, identifier: ctx.input.identifier @@ -39,18 +39,18 @@ export let workflowController = app.controller({ return workflowPresenter(workflow); }), - list: instanceApp + list: tenantApp .handler() .input( Paginator.validate( v.object({ - instanceId: v.string() + tenantId: v.string() }) ) ) .do(async ctx => { let paginator = await workflowService.listWorkflows({ - instance: ctx.instance + tenant: ctx.tenant }); let list = await paginator.run(ctx.input); @@ -62,7 +62,7 @@ export let workflowController = app.controller({ .handler() .input( v.object({ - instanceId: v.string(), + tenantId: v.string(), workflowId: v.string() }) ) @@ -72,7 +72,7 @@ export let workflowController = app.controller({ .handler() .input( v.object({ - instanceId: v.string(), + tenantId: v.string(), workflowId: v.string() }) ) @@ -88,7 +88,7 @@ export let workflowController = app.controller({ .handler() .input( v.object({ - instanceId: v.string(), + tenantId: v.string(), workflowId: v.string(), name: v.optional(v.string()) diff --git a/service/src/controllers/workflowArtifact.ts b/service/src/controllers/workflowArtifact.ts index 4490ff3..4ef38e1 100644 --- a/service/src/controllers/workflowArtifact.ts +++ b/service/src/controllers/workflowArtifact.ts @@ -23,7 +23,7 @@ export let workflowArtifactController = app.controller({ .input( Paginator.validate( v.object({ - instanceId: v.string(), + tenantId: v.string(), workflowId: v.string() }) ) @@ -42,7 +42,7 @@ export let workflowArtifactController = app.controller({ .handler() .input( v.object({ - instanceId: v.string(), + tenantId: v.string(), workflowId: v.string(), workflowArtifactId: v.string() }) diff --git a/service/src/controllers/workflowRun.ts b/service/src/controllers/workflowRun.ts index 46e7325..0ac9ce0 100644 --- a/service/src/controllers/workflowRun.ts +++ b/service/src/controllers/workflowRun.ts @@ -22,7 +22,7 @@ export let workflowRunController = app.controller({ .handler() .input( v.object({ - instanceId: v.string(), + tenantId: v.string(), workflowId: v.string(), env: v.record(v.string()), @@ -54,7 +54,7 @@ export let workflowRunController = app.controller({ Paginator.validate( v.object({ workflowId: v.string(), - instanceId: v.string() + tenantId: v.string() }) ) ) @@ -72,7 +72,7 @@ export let workflowRunController = app.controller({ .handler() .input( v.object({ - instanceId: v.string(), + tenantId: v.string(), workflowId: v.string(), workflowRunId: v.string() }) @@ -83,7 +83,7 @@ export let workflowRunController = app.controller({ .handler() .input( v.object({ - instanceId: v.string(), + tenantId: v.string(), workflowId: v.string(), workflowRunId: v.string() }) @@ -101,7 +101,7 @@ export let workflowRunController = app.controller({ .handler() .input( v.object({ - instanceId: v.string(), + tenantId: v.string(), workflowId: v.string(), workflowRunId: v.string(), workflowRunStepId: v.string() diff --git a/service/src/controllers/workflowVersion.ts b/service/src/controllers/workflowVersion.ts index 1652298..cff8bd2 100644 --- a/service/src/controllers/workflowVersion.ts +++ b/service/src/controllers/workflowVersion.ts @@ -22,7 +22,7 @@ export let workflowVersionController = app.controller({ .handler() .input( v.object({ - instanceId: v.string(), + tenantId: v.string(), workflowId: v.string(), name: v.string(), @@ -69,7 +69,7 @@ export let workflowVersionController = app.controller({ Paginator.validate( v.object({ workflowId: v.string(), - instanceId: v.string() + tenantId: v.string() }) ) ) @@ -87,7 +87,7 @@ export let workflowVersionController = app.controller({ .handler() .input( v.object({ - instanceId: v.string(), + tenantId: v.string(), workflowId: v.string(), workflowVersionId: v.string() }) diff --git a/service/src/id.ts b/service/src/id.ts index d119d4d..9d47353 100644 --- a/service/src/id.ts +++ b/service/src/id.ts @@ -2,7 +2,7 @@ import { createIdGenerator, idType } from '@lowerdeck/id'; import { Worker as SnowflakeId } from 'snowflake-uuid'; export let ID = createIdGenerator({ - instance: idType.sorted('fins_'), + tenant: idType.sorted('fins_'), provider: idType.sorted('fpro_'), diff --git a/service/src/presenters/index.ts b/service/src/presenters/index.ts index 026846d..804a77a 100644 --- a/service/src/presenters/index.ts +++ b/service/src/presenters/index.ts @@ -1,5 +1,5 @@ -export * from './instance'; export * from './provider'; +export * from './tenant'; export * from './workflow'; export * from './workflowArtifact'; export * from './workflowRun'; diff --git a/service/src/presenters/instance.ts b/service/src/presenters/instance.ts deleted file mode 100644 index 46c38a6..0000000 --- a/service/src/presenters/instance.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Instance } from '../../prisma/generated/client'; - -export let instancePresenter = (instance: Instance) => ({ - object: 'instance', - - id: instance.id, - identifier: instance.identifier, - name: instance.name, - - createdAt: instance.createdAt -}); diff --git a/service/src/presenters/tenant.ts b/service/src/presenters/tenant.ts new file mode 100644 index 0000000..1d71b03 --- /dev/null +++ b/service/src/presenters/tenant.ts @@ -0,0 +1,11 @@ +import type { Tenant } from '../../prisma/generated/client'; + +export let tenantPresenter = (tenant: Tenant) => ({ + object: 'tenant', + + id: tenant.id, + identifier: tenant.identifier, + name: tenant.name, + + createdAt: tenant.createdAt +}); diff --git a/service/src/services/index.ts b/service/src/services/index.ts index 026846d..804a77a 100644 --- a/service/src/services/index.ts +++ b/service/src/services/index.ts @@ -1,5 +1,5 @@ -export * from './instance'; export * from './provider'; +export * from './tenant'; export * from './workflow'; export * from './workflowArtifact'; export * from './workflowRun'; diff --git a/service/src/services/instance.ts b/service/src/services/tenant.ts similarity index 58% rename from service/src/services/instance.ts rename to service/src/services/tenant.ts index f7375e3..d240b67 100644 --- a/service/src/services/instance.ts +++ b/service/src/services/tenant.ts @@ -5,19 +5,19 @@ import { ID, snowflake } from '../id'; let include = {}; -class instanceServiceImpl { - async upsertInstance(d: { +class tenantServiceImpl { + async upsertTenant(d: { input: { name: string; identifier: string; }; }) { - return await db.instance.upsert({ + return await db.tenant.upsert({ where: { identifier: d.input.identifier }, update: { name: d.input.name }, create: { oid: snowflake.nextId(), - id: await ID.generateId('instance'), + id: await ID.generateId('tenant'), name: d.input.name, identifier: d.input.identifier }, @@ -25,17 +25,17 @@ class instanceServiceImpl { }); } - async getInstanceById(d: { id: string }) { - let instance = await db.instance.findFirst({ + async getTenantById(d: { id: string }) { + let tenant = await db.tenant.findFirst({ where: { OR: [{ id: d.id }, { identifier: d.id }] }, include }); - if (!instance) throw new ServiceError(notFoundError('instance')); - return instance; + if (!tenant) throw new ServiceError(notFoundError('tenant')); + return tenant; } } -export let instanceService = Service.create( - 'instanceService', - () => new instanceServiceImpl() +export let tenantService = Service.create( + 'tenantService', + () => new tenantServiceImpl() ).build(); diff --git a/service/src/services/workflow.ts b/service/src/services/workflow.ts index 306f4bf..c71445f 100644 --- a/service/src/services/workflow.ts +++ b/service/src/services/workflow.ts @@ -1,7 +1,7 @@ import { notFoundError, ServiceError } from '@lowerdeck/error'; import { Paginator } from '@lowerdeck/pagination'; import { Service } from '@lowerdeck/service'; -import type { Instance, Workflow } from '../../prisma/generated/client'; +import type { Tenant, Workflow } from '../../prisma/generated/client'; import { db } from '../db'; import { ID, snowflake } from '../id'; import { deleteWorkflowQueue } from '../queues/deleteWorkflow'; @@ -15,13 +15,13 @@ class workflowServiceImpl { name: string; identifier: string; }; - instance: Instance; + tenant: Tenant; }) { return await db.workflow.upsert({ where: { - instanceOid_identifier: { + tenantOid_identifier: { identifier: d.input.identifier, - instanceOid: d.instance.oid + tenantOid: d.tenant.oid }, status: 'active' }, @@ -31,7 +31,7 @@ class workflowServiceImpl { id: await ID.generateId('workflow'), name: d.input.name, identifier: d.input.identifier, - instanceOid: d.instance.oid, + tenantOid: d.tenant.oid, status: 'active', providerOid: (await providerService.getDefaultProvider()).oid }, @@ -39,11 +39,11 @@ class workflowServiceImpl { }); } - async getWorkflowById(d: { id: string; instance: Instance }) { + async getWorkflowById(d: { id: string; tenant: Tenant }) { let workflow = await db.workflow.findFirst({ where: { OR: [{ id: d.id }, { identifier: d.id }], - instanceOid: d.instance.oid, + tenantOid: d.tenant.oid, status: 'active' }, include @@ -52,14 +52,14 @@ class workflowServiceImpl { return workflow; } - async listWorkflows(d: { instance: Instance }) { + async listWorkflows(d: { tenant: Tenant }) { return Paginator.create(({ prisma }) => prisma( async opts => await db.workflow.findMany({ ...opts, where: { - instanceOid: d.instance.oid, + tenantOid: d.tenant.oid, status: 'active' } })