diff --git a/.cursor/rules/REST_SERVICE.mdc b/.cursor/rules/REST_SERVICE.mdc index f15c84c..1573228 100644 --- a/.cursor/rules/REST_SERVICE.mdc +++ b/.cursor/rules/REST_SERVICE.mdc @@ -237,6 +237,74 @@ export const authorizedDataSet: Partial> = { } ``` +## Data Access + +### NEVER Use `getStoreManager()` or `StoreManager` Directly + +All data access **must** go through the Repository layer (`getRepository()`) using DataSets. Direct `StoreManager` / `getStoreManager()` usage bypasses authorization and is **forbidden**. + +```typescript +// ❌ FORBIDDEN - bypasses authorization +import { getStoreManager } from '@furystack/core' +const sm = getStoreManager(injector) +const users = await sm.getStoreFor(User, 'username').find({}) + +// ❌ FORBIDDEN - direct StoreManager injection +@Injected(StoreManager) +declare private storeManager: StoreManager + +// ✅ Good - use Repository DataSets +import { getRepository } from '@furystack/repository' +const repository = getRepository(injector) +const users = await repository.getDataSetFor(User, 'username').find(injector, {}) +``` + +### REST Action Handlers + +REST action handlers receive a scoped `injector` with an `IdentityContext` already set up per-request. Pass it directly to DataSet methods: + +```typescript +const MyAction: RequestAction = async ({ injector }) => { + const repository = getRepository(injector) + const items = await repository.getDataSetFor(MyModel, 'id').find(injector, {}) + return JsonResult({ items }) +} +``` + +### Elevated Context for Background Operations + +Background services, middleware, and startup code have no HTTP request context. Use `useSystemIdentityContext()` from `@furystack/core` to create a child injector with system-level privileges: + +```typescript +import { useSystemIdentityContext } from '@furystack/core' +import { getRepository } from '@furystack/repository' +import { usingAsync } from '@furystack/utils' + +// One-off operation (automatic cleanup with usingAsync) +await usingAsync(useSystemIdentityContext({ injector }), async (elevated) => { + const repository = getRepository(elevated) + const items = await repository.getDataSetFor(MyModel, 'id').find(elevated, {}) + await repository.getDataSetFor(MyModel, 'id').update(elevated, id, changes) +}) + +// Singleton services (cache and dispose with service) +@Injectable({ lifetime: 'singleton' }) +export class MyService { + private elevatedInjector?: Injector + + private getElevatedInjector(): Injector { + if (!this.elevatedInjector) { + this.elevatedInjector = useSystemIdentityContext({ injector: getInjectorReference(this) }) + } + return this.elevatedInjector + } + + public async [Symbol.asyncDispose]() { + await this.elevatedInjector?.[Symbol.asyncDispose]() + } +} +``` + ## Store Types ### FileSystemStore @@ -316,26 +384,25 @@ void attachShutdownHandler(injector) ### Seed Script -Create a seed script for initial data: +Create a seed script for initial data using elevated context: ```typescript // service/src/seed.ts -import { StoreManager } from '@furystack/core' +import { useSystemIdentityContext } from '@furystack/core' +import { getRepository } from '@furystack/repository' +import { usingAsync } from '@furystack/utils' import { PasswordAuthenticator, PasswordCredential } from '@furystack/security' import { User } from 'common' import { injector } from './config.js' export const seed = async (i: Injector): Promise => { - const sm = i.getInstance(StoreManager) - const userStore = sm.getStoreFor(User, 'username') - const pwcStore = sm.getStoreFor(PasswordCredential, 'userName') - - // Create default user credentials - const cred = await i.getInstance(PasswordAuthenticator).hasher.createCredential('testuser', 'password') + await usingAsync(useSystemIdentityContext({ injector: i }), async (elevated) => { + const repository = getRepository(elevated) + const cred = await i.getInstance(PasswordAuthenticator).hasher.createCredential('testuser', 'password') - // Save to stores - await pwcStore.add(cred) - await userStore.add({ username: 'testuser', roles: [] }) + await repository.getDataSetFor(PasswordCredential, 'userName').add(elevated, cred) + await repository.getDataSetFor(User, 'username').add(elevated, { username: 'testuser', roles: [] }) + }) } await seed(injector) @@ -375,16 +442,21 @@ useRestService({ 3. **Validate requests** - Use `Validate` wrapper with JSON schemas 4. **Configure stores** - `FileSystemStore` for persistence, `InMemoryStore` for sessions 5. **Handle authorization** - Define authorization functions for data sets -6. **Graceful shutdown** - Implement proper cleanup with `Symbol.asyncDispose` -7. **CORS setup** - Configure for frontend origins +6. **NEVER use `getStoreManager()` or `StoreManager` directly** - Always use `getRepository().getDataSetFor()` for data access +7. **Use `useSystemIdentityContext()` from `@furystack/core`** for background services, middleware, and startup operations that lack an HTTP request context +8. **Graceful shutdown** - Implement proper cleanup with `Symbol.asyncDispose` +9. **CORS setup** - Configure for frontend origins **Service Checklist:** - [ ] API types defined in `common` package - [ ] JSON schemas generated for validation - [ ] Stores configured for all models +- [ ] DataSets created for all models via `getRepository().createDataSet()` - [ ] Authentication set up with `useHttpAuthentication` - [ ] Authorization functions defined +- [ ] No `getStoreManager()` or `StoreManager` usage — all data access via Repository +- [ ] Background services use `useSystemIdentityContext()` for data access - [ ] CORS configured for frontend - [ ] Graceful shutdown handler attached - [ ] Error handling for startup failures diff --git a/.cursor/rules/SHADES_COMPONENTS.mdc b/.cursor/rules/SHADES_COMPONENTS.mdc index ea9e0f1..0d6386c 100644 --- a/.cursor/rules/SHADES_COMPONENTS.mdc +++ b/.cursor/rules/SHADES_COMPONENTS.mdc @@ -210,6 +210,100 @@ export const LoginForm = Shade({ }); ``` +## Form Handling + +### Always use `Form` from `@furystack/shades-common-components` + +Never use raw `
` HTML elements. The `Form` component provides type-safe form data collection, two-tier validation (input + form level), and integration with all form field components (`Input`, `Select`, `Checkbox`, etc.). + +### Pattern: Typed Form Payload + +Every form needs: + +1. A **payload type** describing the form data shape +2. A **type-guard `validate` function** that narrows `unknown` to the payload type +3. A `Form` component with `validate` and `onSubmit` props + +```typescript +import { Form, Input, Button } from '@furystack/shades-common-components'; + +type CreateStackPayload = { + name: string; + displayName: string; + description: string; + mainDirectory: string; +}; + +const isCreateStackPayload = (data: unknown): data is CreateStackPayload => { + const d = data as CreateStackPayload; + return d.name?.length > 0 && d.displayName?.length > 0 && d.mainDirectory?.length > 0; +}; + +// In render: + + validate={isCreateStackPayload} + onSubmit={(data) => { + // `data` is fully typed as CreateStackPayload + void handleSubmit(data); + }} + style={{ display: 'flex', flexDirection: 'column', gap: '16px' }} +> + + + + + + +``` + +### Use `Checkbox` component instead of raw `` + +```typescript +import { Checkbox } from '@furystack/shades-common-components'; + +// ✅ Good - uses Checkbox component, integrates with Form + + +// ❌ Avoid - raw checkbox, no FormService integration + +``` + +### Forbidden Patterns + +Never use these patterns for form data handling: + +```typescript +// ❌ FORBIDDEN - raw
tag + { ... }}> + +// ❌ FORBIDDEN - FormData extraction from DOM +const formData = new FormData(ev.target as HTMLFormElement); +const data = Object.fromEntries(formData.entries()) as Record; + +// ❌ FORBIDDEN - useRef to imperatively read form data +const formRef = useRef('myForm'); +const data = new FormData(formRef.current); + +// ❌ FORBIDDEN - direct DOM value access for form fields +oninput={(ev) => setValue((ev.target as HTMLInputElement).value)} +``` + +### Form Field Components + +Use these components from `@furystack/shades-common-components` inside `Form`: + +- `Input` - Text, number, email, password fields +- `Select` - Single/multi select dropdowns +- `Checkbox` - Boolean checkboxes +- `TextArea` - Multi-line text input +- `Switch` - Toggle switches +- `Radio` / `RadioGroup` - Radio button groups + +All form field components automatically register with the parent `Form` via the `FormService` and participate in validation. + ## Theming ### Access Theme Properties @@ -332,6 +426,7 @@ initializeShadeRoot({ 6. **useDisposable for cleanup** - Manage subscriptions properly 7. **Use common components** - Leverage `@furystack/shades-common-components` 8. **Consistent theming** - Use `ThemeProviderService` for styles +9. **Use `Form` for all forms** - Never use raw `` tags, `FormData`, or `useRef` for form handling **Component Checklist:** @@ -341,3 +436,4 @@ initializeShadeRoot({ - [ ] Observable subscriptions use `useObservable` - [ ] Manual subscriptions cleaned up with `useDisposable` - [ ] Theme values from `ThemeProviderService` +- [ ] Forms use `Form` with typed payload and validate function diff --git a/.gitignore b/.gitignore index 73d183a..7858d0b 100644 --- a/.gitignore +++ b/.gitignore @@ -64,7 +64,22 @@ dist *.tsbuildinfo frontend/bundle/js +# TypeScript compilation artifacts for config/spec files +e2e/*.js +e2e/*.d.ts +e2e/*.js.map +frontend/vite.config.js +frontend/vite.config.d.ts +frontend/vite.config.js.map +playwright.config.js +playwright.config.d.ts +playwright.config.js.map +vitest.config.mjs +vitest.config.d.mts +vitest.config.mjs.map + service/data.sqlite +service/data/ testresults .pnp.* diff --git a/.yarn/versions/0de640ed.yml b/.yarn/versions/0de640ed.yml new file mode 100644 index 0000000..4b638da --- /dev/null +++ b/.yarn/versions/0de640ed.yml @@ -0,0 +1,5 @@ +releases: + common: patch + frontend: patch + service: patch + stack-craft: patch diff --git a/common/package.json b/common/package.json index 5532994..a4ad8b5 100644 --- a/common/package.json +++ b/common/package.json @@ -25,11 +25,12 @@ "create-schemas": "node ./dist/bin/create-schemas.js" }, "devDependencies": { - "@types/node": "^25.2.3", + "@types/node": "^25.3.0", "ts-json-schema-generator": "^2.5.0", "vitest": "^4.0.18" }, "dependencies": { - "@furystack/rest": "^8.0.36" + "@furystack/core": "^15.2.0", + "@furystack/rest": "^8.0.38" } } diff --git a/common/schemas/entities.json b/common/schemas/entities.json index ec29351..05ade07 100644 --- a/common/schemas/entities.json +++ b/common/schemas/entities.json @@ -1,7 +1,82 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$ref": "#/definitions/User", "definitions": { + "ApiToken": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "name": { + "type": "string" + }, + "tokenHash": { + "type": "string" + }, + "lastUsedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "required": ["id", "username", "name", "tokenHash", "createdAt"], + "additionalProperties": false + }, + "EnvironmentVariableValue": { + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": ["inherit", "custom"], + "description": "Whether to inherit the value from the host system or use a custom value" + }, + "customValue": { + "type": "string", + "description": "The custom value to use when source is 'custom'" + } + }, + "required": ["source"], + "additionalProperties": false, + "description": "Describes how an environment variable's value is resolved. Used in {@link StackConfig } for stack-level defaults and in {@link ServiceConfig } for per-service overrides." + }, + "GitHubRepository": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "url": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": ["id", "stackName", "url", "displayName", "description", "createdAt", "updatedAt"], + "additionalProperties": false, + "description": "Shareable GitHub repository definition. Links a git repository to a stack for cloning and pulling. Included in stack exports and shared between installations." + }, "User": { "type": "object", "properties": { @@ -17,6 +92,850 @@ }, "required": ["username", "roles"], "additionalProperties": false + }, + "StackDefinition": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this stack does" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": ["name", "displayName", "description", "createdAt", "updatedAt"], + "additionalProperties": false, + "description": "Shareable stack definition. Contains the immutable identity and description of a stack. Included in stack exports and shared between installations." + }, + "StackConfig": { + "type": "object", + "properties": { + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "mainDirectory": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + }, + "environmentVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Stack-level environment variable values, keyed by variable name" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": ["stackName", "mainDirectory", "environmentVariables", "createdAt", "updatedAt"], + "additionalProperties": false, + "description": "User-specific stack configuration. Contains settings unique to this installation/machine. Not included in exports - set by the user during import/installation." + }, + "ServiceFile": { + "type": "object", + "properties": { + "relativePath": { + "type": "string", + "description": "Path relative to the service working directory" + }, + "content": { + "type": "string", + "description": "File content (usually plain text)" + } + }, + "required": ["relativePath", "content"], + "additionalProperties": false, + "description": "A file to be placed relative to the service root (e.g. .env, appConfig.local.json)" + }, + "ServiceDefinition": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this service does" + }, + "workingDirectory": { + "type": "string", + "description": "Optional relative path within stack for grouping (e.g. \"frontends/public\")" + }, + "repositoryId": { + "type": "string", + "description": "Optional FK to {@link GitHubRepository.id }" + }, + "prerequisiteIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of {@link Prerequisite } entities required by this service" + }, + "prerequisiteServiceIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of other {@link ServiceDefinition } entities that must be running first" + }, + "installCommand": { + "type": "string", + "description": "Shell command to install dependencies (e.g. \"npm install\")" + }, + "buildCommand": { + "type": "string", + "description": "Shell command to build the service (e.g. \"npm run build\")" + }, + "runCommand": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceFile" + }, + "description": "Shared files placed relative to the service root" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "id", + "stackName", + "displayName", + "description", + "prerequisiteIds", + "prerequisiteServiceIds", + "runCommand", + "files", + "createdAt", + "updatedAt" + ], + "additionalProperties": false, + "description": "Shareable service definition. Contains the immutable description of a service and its commands. Included in stack exports and shared between installations." + }, + "ServiceConfig": { + "type": "object", + "properties": { + "serviceId": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + }, + "autoFetchEnabled": { + "type": "boolean", + "description": "Whether automatic git fetch is enabled" + }, + "autoFetchIntervalMinutes": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + }, + "autoRestartOnFetch": { + "type": "boolean", + "description": "Whether to automatically restart the service when new commits are fetched" + }, + "environmentVariableOverrides": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Per-service environment variable overrides, keyed by variable name. Overrides stack-level defaults." + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": [ + "serviceId", + "autoFetchEnabled", + "autoFetchIntervalMinutes", + "autoRestartOnFetch", + "environmentVariableOverrides", + "createdAt", + "updatedAt" + ], + "additionalProperties": false, + "description": "User-specific service configuration. Contains settings that each installation can customize independently. Not included in exports - set by the user during import/installation." + }, + "CloneStatus": { + "type": "string", + "enum": ["not-cloned", "cloning", "cloned", "failed"] + }, + "InstallStatus": { + "type": "string", + "enum": ["not-installed", "installing", "installed", "failed"] + }, + "BuildStatus": { + "type": "string", + "enum": ["not-built", "building", "built", "failed"] + }, + "RunStatus": { + "type": "string", + "enum": ["stopped", "starting", "running", "stopping", "error"] + }, + "ServiceStatus": { + "type": "object", + "properties": { + "serviceId": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + }, + "cloneStatus": { + "$ref": "#/definitions/CloneStatus" + }, + "installStatus": { + "$ref": "#/definitions/InstallStatus" + }, + "buildStatus": { + "$ref": "#/definitions/BuildStatus" + }, + "runStatus": { + "$ref": "#/definitions/RunStatus" + }, + "lastClonedAt": { + "type": "string" + }, + "lastInstalledAt": { + "type": "string" + }, + "lastBuiltAt": { + "type": "string" + }, + "lastStartedAt": { + "type": "string" + }, + "lastFetchedAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": ["serviceId", "cloneStatus", "installStatus", "buildStatus", "runStatus", "updatedAt"], + "additionalProperties": false, + "description": "Runtime status of a service. Managed by the system (ProcessManager, GitWatcher). Never exported. Reset to defaults on import." + }, + "ServiceStateEvent": { + "type": "string", + "enum": [ + "clone-started", + "clone-completed", + "clone-failed", + "run-started", + "run-stopped", + "run-crashed", + "run-restarted", + "install-started", + "install-completed", + "install-failed", + "build-started", + "build-completed", + "build-failed", + "setup-started", + "setup-completed", + "setup-failed", + "update-started", + "update-completed", + "update-failed", + "pull-completed", + "imported", + "state-reconciled" + ], + "description": "Event types for service state transitions. Each value represents a discrete lifecycle event that can occur." + }, + "TriggerSource": { + "type": "string", + "enum": ["api", "mcp", "auto-fetch", "auto-restart", "system"], + "description": "How a state change was triggered. Used to distinguish user actions from automated system behavior." + }, + "ServiceStateHistory": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Auto-increment primary key" + }, + "serviceId": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + }, + "event": { + "$ref": "#/definitions/ServiceStateEvent", + "description": "The lifecycle event that occurred" + }, + "previousState": { + "type": "string", + "description": "JSON snapshot of relevant status fields before the change" + }, + "newState": { + "type": "string", + "description": "JSON snapshot of relevant status fields after the change" + }, + "triggeredBy": { + "type": "string", + "description": "Username of the user who triggered the action, or 'system'" + }, + "triggerSource": { + "$ref": "#/definitions/TriggerSource", + "description": "How the action was triggered" + }, + "metadata": { + "type": "string", + "description": "Optional JSON with extra context (exit code, error message, etc.)" + }, + "processUid": { + "type": "string", + "description": "UUID of the associated process, if this event produced log output" + }, + "createdAt": { + "type": "string" + } + }, + "required": ["id", "serviceId", "event", "triggeredBy", "triggerSource", "createdAt"], + "additionalProperties": false, + "description": "Audit log entry for service state transitions. Records every lifecycle event (start, stop, crash, install, build, pull) with full context: who triggered it, how, and any relevant metadata. Entries are never deleted and not included in exports." + }, + "ServiceLogEntry": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "serviceId": { + "type": "string" + }, + "processUid": { + "type": "string" + }, + "stream": { + "type": "string", + "enum": ["stdout", "stderr"] + }, + "line": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "required": ["id", "serviceId", "processUid", "stream", "line", "createdAt"], + "additionalProperties": false + }, + "PrerequisiteType": { + "type": "string", + "enum": [ + "node", + "yarn", + "dotnet-sdk", + "dotnet-runtime", + "nuget-feed", + "git", + "github-cli", + "env-variable", + "custom-script" + ], + "description": "The type of prerequisite that must be satisfied before a stack or service can run." + }, + "PrerequisiteConfigMap": { + "type": "object", + "properties": { + "node": { + "type": "object", + "properties": { + "minimumVersion": { + "type": "string" + } + }, + "required": ["minimumVersion"], + "additionalProperties": false + }, + "yarn": { + "type": "object", + "properties": { + "minimumVersion": { + "type": "string" + } + }, + "required": ["minimumVersion"], + "additionalProperties": false + }, + "dotnet-sdk": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + }, + "dotnet-runtime": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + }, + "nuget-feed": { + "type": "object", + "properties": { + "feedUrl": { + "type": "string" + }, + "feedName": { + "type": "string" + } + }, + "required": ["feedUrl"], + "additionalProperties": false + }, + "git": { + "type": "object", + "additionalProperties": { + "not": {} + } + }, + "github-cli": { + "type": "object", + "additionalProperties": { + "not": {} + } + }, + "env-variable": { + "type": "object", + "properties": { + "variableName": { + "type": "string" + } + }, + "required": ["variableName"], + "additionalProperties": false + }, + "custom-script": { + "type": "object", + "properties": { + "script": { + "type": "string" + } + }, + "required": ["script"], + "additionalProperties": false + } + }, + "required": [ + "node", + "yarn", + "dotnet-sdk", + "dotnet-runtime", + "nuget-feed", + "git", + "github-cli", + "env-variable", + "custom-script" + ], + "additionalProperties": false, + "description": "Maps each prerequisite type to its type-specific configuration shape." + }, + "PrerequisiteConfig": { + "anyOf": [ + { + "type": "object", + "properties": { + "minimumVersion": { + "type": "string" + } + }, + "required": ["minimumVersion"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "minimumVersion": { + "type": "string" + } + }, + "required": ["minimumVersion"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "feedUrl": { + "type": "string" + }, + "feedName": { + "type": "string" + } + }, + "required": ["feedUrl"], + "additionalProperties": false + }, + { + "type": "object", + "additionalProperties": { + "not": {} + } + }, + { + "type": "object", + "properties": { + "variableName": { + "type": "string" + } + }, + "required": ["variableName"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "script": { + "type": "string" + } + }, + "required": ["script"], + "additionalProperties": false + } + ], + "description": "Union of all possible prerequisite config shapes." + }, + "Prerequisite": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "name": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + }, + "type": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + }, + "config": { + "$ref": "#/definitions/PrerequisiteConfig", + "description": "Type-specific configuration (stored as JSON TEXT in the database)" + }, + "installationHelp": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": ["id", "stackName", "name", "type", "config", "installationHelp", "createdAt", "updatedAt"], + "additionalProperties": false, + "description": "Shareable prerequisite definition. Describes an external requirement (e.g. Node.js, Git, an env variable) that a stack or service may need to be satisfied before it can run. Included in stack exports and shared between installations." + }, + "PrerequisiteCheckStatus": { + "type": "string", + "enum": ["unchecked", "checking", "satisfied", "failed"], + "description": "Possible statuses for a prerequisite check." + }, + "PrerequisiteCheckResult": { + "type": "object", + "properties": { + "prerequisiteId": { + "type": "string", + "description": "PK – corresponds to {@link Prerequisite.id }" + }, + "status": { + "$ref": "#/definitions/PrerequisiteCheckStatus", + "description": "Current check status" + }, + "output": { + "type": "string", + "description": "Human-readable output from the last check" + }, + "checkedAt": { + "type": "string", + "description": "ISO-8601 timestamp of the last check" + } + }, + "required": ["prerequisiteId", "status", "output", "checkedAt"], + "additionalProperties": false, + "description": "Represents the result of evaluating a {@link Prerequisite } . Stored in an in-memory store on the service and synced to the frontend via entity sync so that every connected client sees real-time status." + }, + "PublicApiToken": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "name": { + "type": "string" + }, + "lastUsedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "required": ["id", "username", "name", "createdAt"], + "additionalProperties": false + }, + "StackView": { + "type": "object", + "additionalProperties": false, + "properties": { + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "mainDirectory": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + }, + "environmentVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Stack-level environment variable values, keyed by variable name" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "name": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this stack does" + } + }, + "required": [ + "createdAt", + "description", + "displayName", + "environmentVariables", + "mainDirectory", + "name", + "stackName", + "updatedAt" + ], + "description": "Full stack view combining definition and config for API responses" + }, + "ServiceView": { + "type": "object", + "additionalProperties": false, + "properties": { + "serviceId": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + }, + "cloneStatus": { + "$ref": "#/definitions/CloneStatus" + }, + "installStatus": { + "$ref": "#/definitions/InstallStatus" + }, + "buildStatus": { + "$ref": "#/definitions/BuildStatus" + }, + "runStatus": { + "$ref": "#/definitions/RunStatus" + }, + "lastClonedAt": { + "type": "string" + }, + "lastInstalledAt": { + "type": "string" + }, + "lastBuiltAt": { + "type": "string" + }, + "lastStartedAt": { + "type": "string" + }, + "lastFetchedAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "autoFetchEnabled": { + "type": "boolean", + "description": "Whether automatic git fetch is enabled" + }, + "autoFetchIntervalMinutes": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + }, + "autoRestartOnFetch": { + "type": "boolean", + "description": "Whether to automatically restart the service when new commits are fetched" + }, + "environmentVariableOverrides": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Per-service environment variable overrides, keyed by variable name. Overrides stack-level defaults." + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this service does" + }, + "workingDirectory": { + "type": "string", + "description": "Optional relative path within stack for grouping (e.g. \"frontends/public\")" + }, + "repositoryId": { + "type": "string", + "description": "Optional FK to {@link GitHubRepository.id }" + }, + "prerequisiteIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of {@link Prerequisite } entities required by this service" + }, + "prerequisiteServiceIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of other {@link ServiceDefinition } entities that must be running first" + }, + "installCommand": { + "type": "string", + "description": "Shell command to install dependencies (e.g. \"npm install\")" + }, + "buildCommand": { + "type": "string", + "description": "Shell command to build the service (e.g. \"npm run build\")" + }, + "runCommand": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceFile" + }, + "description": "Shared files placed relative to the service root" + } + }, + "required": [ + "autoFetchEnabled", + "autoFetchIntervalMinutes", + "autoRestartOnFetch", + "buildStatus", + "cloneStatus", + "createdAt", + "description", + "displayName", + "environmentVariableOverrides", + "files", + "id", + "installStatus", + "prerequisiteIds", + "prerequisiteServiceIds", + "runCommand", + "runStatus", + "serviceId", + "stackName", + "updatedAt" + ], + "description": "Full service view combining definition, config, and status for API responses" } } } diff --git a/common/schemas/github-repositories-api.json b/common/schemas/github-repositories-api.json new file mode 100644 index 0000000..7f0e30a --- /dev/null +++ b/common/schemas/github-repositories-api.json @@ -0,0 +1,908 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "GitHubRepoWritableFields": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "url": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description" + } + }, + "required": ["id", "stackName", "url", "displayName", "description"], + "additionalProperties": false + }, + "PostGitHubRepoEndpoint": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/GitHubRepository" + }, + "body": { + "$ref": "#/definitions/WithOptionalId%3CGitHubRepoWritableFields%2C%22id%22%3E" + } + }, + "required": ["result", "body"], + "additionalProperties": false + }, + "GitHubRepository": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "url": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": ["id", "stackName", "url", "displayName", "description", "createdAt", "updatedAt"], + "additionalProperties": false, + "description": "Shareable GitHub repository definition. Links a git repository to a stack for cloning and pulling. Included in stack exports and shared between installations." + }, + "WithOptionalId": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "url": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description" + } + }, + "required": ["description", "displayName", "stackName", "url"] + }, + "PatchGitHubRepoEndpoint": { + "$ref": "#/definitions/PatchEndpoint%3CGitHubRepoWritableFields%2C%22id%22%3E" + }, + "PatchEndpoint": { + "type": "object", + "properties": { + "body": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "url": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description" + } + }, + "additionalProperties": false + }, + "url": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object" + } + }, + "required": ["body", "url", "result"], + "additionalProperties": false, + "description": "Endpoint model for updating entities" + }, + "ValidateRepoEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object", + "properties": { + "accessible": { + "type": "boolean" + }, + "message": { + "type": "string" + } + }, + "required": ["accessible"], + "additionalProperties": false + } + }, + "required": ["url", "result"], + "additionalProperties": false + }, + "GitHubRepositoriesApi": { + "type": "object", + "properties": { + "GET": { + "type": "object", + "properties": { + "/github-repositories": { + "$ref": "#/definitions/GetCollectionEndpoint%3CGitHubRepository%3E" + }, + "/github-repositories/:id": { + "$ref": "#/definitions/GetEntityEndpoint%3CGitHubRepository%2C%22id%22%3E" + } + }, + "required": ["/github-repositories", "/github-repositories/:id"], + "additionalProperties": false + }, + "POST": { + "type": "object", + "properties": { + "/github-repositories": { + "$ref": "#/definitions/PostGitHubRepoEndpoint" + }, + "/github-repositories/:id/validate": { + "$ref": "#/definitions/ValidateRepoEndpoint" + } + }, + "required": ["/github-repositories", "/github-repositories/:id/validate"], + "additionalProperties": false + }, + "PATCH": { + "type": "object", + "properties": { + "/github-repositories/:id": { + "$ref": "#/definitions/PatchGitHubRepoEndpoint" + } + }, + "required": ["/github-repositories/:id"], + "additionalProperties": false + }, + "PUT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "DELETE": { + "type": "object", + "properties": { + "/github-repositories/:id": { + "$ref": "#/definitions/DeleteEndpoint%3CGitHubRepository%2C%22id%22%3E" + } + }, + "required": ["/github-repositories/:id"], + "additionalProperties": false + }, + "HEAD": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "CONNECT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "TRACE": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "OPTIONS": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + } + }, + "required": ["GET", "POST", "PATCH", "DELETE"], + "additionalProperties": false + }, + "GetCollectionEndpoint": { + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "findOptions": { + "$ref": "#/definitions/FindOptions%3CGitHubRepository%2C(%22id%22%7C%22stackName%22%7C%22url%22%7C%22displayName%22%7C%22description%22%7C%22createdAt%22%7C%22updatedAt%22)%5B%5D%3E" + } + }, + "additionalProperties": false + }, + "result": { + "$ref": "#/definitions/GetCollectionResult%3CGitHubRepository%3E" + } + }, + "required": ["query", "result"], + "additionalProperties": false, + "description": "Rest endpoint model for getting / querying collections" + }, + "FindOptions": { + "type": "object", + "properties": { + "top": { + "type": "number", + "description": "Limits the hits" + }, + "skip": { + "type": "number", + "description": "Skips the first N hit" + }, + "order": { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "stackName": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "url": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "displayName": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "description": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "createdAt": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "updatedAt": { + "type": "string", + "enum": ["ASC", "DESC"] + } + }, + "additionalProperties": false, + "description": "Sets up an order by a field and a direction" + }, + "select": { + "type": "array", + "items": { + "type": "string", + "enum": ["id", "stackName", "url", "displayName", "description", "createdAt", "updatedAt"] + }, + "description": "The result set will be limited to these fields" + }, + "filter": { + "$ref": "#/definitions/FilterType%3CGitHubRepository%3E", + "description": "The fields should match this filter" + } + }, + "additionalProperties": false, + "description": "Type for default filtering model" + }, + "FilterType": { + "type": "object", + "additionalProperties": false, + "properties": { + "$and": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CGitHubRepository%3E" + } + }, + "$not": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CGitHubRepository%3E" + } + }, + "$nor": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CGitHubRepository%3E" + } + }, + "$or": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CGitHubRepository%3E" + } + }, + "id": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "UUID primary key" + }, + "$endsWith": { + "type": "string", + "description": "UUID primary key" + }, + "$like": { + "type": "string", + "description": "UUID primary key" + }, + "$regex": { + "type": "string", + "description": "UUID primary key" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "UUID primary key" + }, + "$ne": { + "type": "string", + "description": "UUID primary key" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "UUID primary key" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "UUID primary key" + } + } + }, + "additionalProperties": false + } + ] + }, + "stackName": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$endsWith": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$like": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$regex": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$ne": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + } + }, + "additionalProperties": false + } + ] + }, + "url": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + }, + "$endsWith": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + }, + "$like": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + }, + "$regex": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + }, + "$ne": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + } + } + }, + "additionalProperties": false + } + ] + }, + "displayName": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "$endsWith": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "$like": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "$regex": { + "type": "string", + "description": "Human-readable name shown in the UI" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "$ne": { + "type": "string", + "description": "Human-readable name shown in the UI" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "Human-readable name shown in the UI" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "Human-readable name shown in the UI" + } + } + }, + "additionalProperties": false + } + ] + }, + "description": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "Optional description" + }, + "$endsWith": { + "type": "string", + "description": "Optional description" + }, + "$like": { + "type": "string", + "description": "Optional description" + }, + "$regex": { + "type": "string", + "description": "Optional description" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Optional description" + }, + "$ne": { + "type": "string", + "description": "Optional description" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "Optional description" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "Optional description" + } + } + }, + "additionalProperties": false + } + ] + }, + "createdAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string" + }, + "$endsWith": { + "type": "string" + }, + "$like": { + "type": "string" + }, + "$regex": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + ] + }, + "updatedAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string" + }, + "$endsWith": { + "type": "string" + }, + "$like": { + "type": "string" + }, + "$regex": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + ] + } + } + }, + "GetCollectionResult": { + "type": "object", + "properties": { + "count": { + "type": "number", + "description": "The Total count of entities" + }, + "entries": { + "type": "array", + "items": { + "$ref": "#/definitions/GitHubRepository" + }, + "description": "List of the selected entities" + } + }, + "required": ["count", "entries"], + "additionalProperties": false, + "description": "Response Model for GetCollection" + }, + "GetEntityEndpoint": { + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "select": { + "type": "array", + "items": { + "type": "string", + "enum": ["id", "stackName", "url", "displayName", "description", "createdAt", "updatedAt"] + }, + "description": "The list of fields to select" + } + }, + "additionalProperties": false + }, + "url": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The entity's unique identifier" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "$ref": "#/definitions/GitHubRepository" + } + }, + "required": ["query", "url", "result"], + "additionalProperties": false, + "description": "Endpoint model for getting a single entity" + }, + "DeleteEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object" + } + }, + "required": ["url", "result"], + "additionalProperties": false, + "description": "Endpoint model for deleting entities" + } + } +} diff --git a/common/schemas/identity-api.json b/common/schemas/identity-api.json new file mode 100644 index 0000000..4b44e6a --- /dev/null +++ b/common/schemas/identity-api.json @@ -0,0 +1,250 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "IsAuthenticatedAction": { + "type": "object", + "properties": { + "result": { + "type": "object", + "properties": { + "isAuthenticated": { + "type": "boolean" + } + }, + "required": ["isAuthenticated"], + "additionalProperties": false + } + }, + "required": ["result"], + "additionalProperties": false + }, + "GetCurrentUserAction": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/User" + } + }, + "required": ["result"], + "additionalProperties": false + }, + "User": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["username", "roles"], + "additionalProperties": false + }, + "LoginAction": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/User" + }, + "body": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": ["username", "password"], + "additionalProperties": false + } + }, + "required": ["result", "body"], + "additionalProperties": false + }, + "LogoutAction": { + "type": "object", + "properties": { + "result": {} + }, + "required": ["result"], + "additionalProperties": false + }, + "PasswordResetAction": { + "type": "object", + "properties": { + "result": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"], + "additionalProperties": false + }, + "body": { + "type": "object", + "properties": { + "currentPassword": { + "type": "string" + }, + "newPassword": { + "type": "string" + } + }, + "required": ["currentPassword", "newPassword"], + "additionalProperties": false + } + }, + "required": ["result", "body"], + "additionalProperties": false + }, + "IdentityApi": { + "type": "object", + "properties": { + "GET": { + "type": "object", + "properties": { + "/isAuthenticated": { + "$ref": "#/definitions/IsAuthenticatedAction" + }, + "/currentUser": { + "$ref": "#/definitions/GetCurrentUserAction" + } + }, + "required": ["/isAuthenticated", "/currentUser"], + "additionalProperties": false + }, + "POST": { + "type": "object", + "properties": { + "/login": { + "$ref": "#/definitions/LoginAction" + }, + "/logout": { + "$ref": "#/definitions/LogoutAction" + }, + "/password-reset": { + "$ref": "#/definitions/PasswordResetAction" + } + }, + "required": ["/login", "/logout", "/password-reset"], + "additionalProperties": false + }, + "PATCH": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "PUT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "DELETE": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "HEAD": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "CONNECT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "TRACE": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "OPTIONS": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + } + }, + "required": ["GET", "POST"], + "additionalProperties": false + } + } +} diff --git a/common/schemas/install-api.json b/common/schemas/install-api.json new file mode 100644 index 0000000..71596b3 --- /dev/null +++ b/common/schemas/install-api.json @@ -0,0 +1,191 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "InstallState": { + "type": "string", + "enum": ["needsInstall", "installed"] + }, + "InstallStateResponse": { + "type": "object", + "properties": { + "state": { + "$ref": "#/definitions/InstallState" + } + }, + "required": ["state"], + "additionalProperties": false + }, + "GetServiceStatusAction": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/InstallStateResponse" + } + }, + "required": ["result"], + "additionalProperties": false + }, + "InstallAction": { + "type": "object", + "properties": { + "result": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"], + "additionalProperties": false + }, + "body": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + }, + "required": ["username", "password"], + "additionalProperties": false + } + }, + "required": ["result", "body"], + "additionalProperties": false + }, + "InstallApi": { + "type": "object", + "properties": { + "GET": { + "type": "object", + "properties": { + "/serviceStatus": { + "$ref": "#/definitions/GetServiceStatusAction" + } + }, + "required": ["/serviceStatus"], + "additionalProperties": false + }, + "POST": { + "type": "object", + "properties": { + "/install": { + "$ref": "#/definitions/InstallAction" + } + }, + "required": ["/install"], + "additionalProperties": false + }, + "PATCH": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "PUT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "DELETE": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "HEAD": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "CONNECT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "TRACE": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "OPTIONS": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + } + }, + "required": ["GET", "POST"], + "additionalProperties": false + } + } +} diff --git a/common/schemas/prerequisites-api.json b/common/schemas/prerequisites-api.json new file mode 100644 index 0000000..e25f30e --- /dev/null +++ b/common/schemas/prerequisites-api.json @@ -0,0 +1,1065 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "PrerequisiteWritableFields": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "name": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + }, + "type": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + }, + "config": { + "$ref": "#/definitions/PrerequisiteConfig", + "description": "Type-specific configuration (stored as JSON TEXT in the database)" + }, + "installationHelp": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + } + }, + "required": ["id", "stackName", "name", "type", "config", "installationHelp"], + "additionalProperties": false + }, + "PrerequisiteType": { + "type": "string", + "enum": [ + "node", + "yarn", + "dotnet-sdk", + "dotnet-runtime", + "nuget-feed", + "git", + "github-cli", + "env-variable", + "custom-script" + ], + "description": "The type of prerequisite that must be satisfied before a stack or service can run." + }, + "PrerequisiteConfig": { + "anyOf": [ + { + "type": "object", + "properties": { + "minimumVersion": { + "type": "string" + } + }, + "required": ["minimumVersion"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "minimumVersion": { + "type": "string" + } + }, + "required": ["minimumVersion"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "feedUrl": { + "type": "string" + }, + "feedName": { + "type": "string" + } + }, + "required": ["feedUrl"], + "additionalProperties": false + }, + { + "type": "object", + "additionalProperties": { + "not": {} + } + }, + { + "type": "object", + "properties": { + "variableName": { + "type": "string" + } + }, + "required": ["variableName"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "script": { + "type": "string" + } + }, + "required": ["script"], + "additionalProperties": false + } + ], + "description": "Union of all possible prerequisite config shapes." + }, + "PostPrerequisiteEndpoint": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/Prerequisite" + }, + "body": { + "$ref": "#/definitions/WithOptionalId%3CPrerequisiteWritableFields%2C%22id%22%3E" + } + }, + "required": ["result", "body"], + "additionalProperties": false + }, + "Prerequisite": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "name": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + }, + "type": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + }, + "config": { + "$ref": "#/definitions/PrerequisiteConfig", + "description": "Type-specific configuration (stored as JSON TEXT in the database)" + }, + "installationHelp": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + } + }, + "required": ["id", "stackName", "name", "type", "config", "installationHelp", "createdAt", "updatedAt"], + "additionalProperties": false, + "description": "Shareable prerequisite definition. Describes an external requirement (e.g. Node.js, Git, an env variable) that a stack or service may need to be satisfied before it can run. Included in stack exports and shared between installations." + }, + "WithOptionalId": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "name": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + }, + "type": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + }, + "config": { + "$ref": "#/definitions/PrerequisiteConfig", + "description": "Type-specific configuration (stored as JSON TEXT in the database)" + }, + "installationHelp": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + } + }, + "required": ["config", "installationHelp", "name", "stackName", "type"] + }, + "PatchPrerequisiteEndpoint": { + "$ref": "#/definitions/PatchEndpoint%3CPrerequisiteWritableFields%2C%22id%22%3E" + }, + "PatchEndpoint": { + "type": "object", + "properties": { + "body": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "name": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + }, + "type": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + }, + "config": { + "$ref": "#/definitions/PrerequisiteConfig", + "description": "Type-specific configuration (stored as JSON TEXT in the database)" + }, + "installationHelp": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + } + }, + "additionalProperties": false + }, + "url": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object" + } + }, + "required": ["body", "url", "result"], + "additionalProperties": false, + "description": "Endpoint model for updating entities" + }, + "CheckPrerequisiteEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object", + "properties": { + "satisfied": { + "type": "boolean" + }, + "output": { + "type": "string" + } + }, + "required": ["satisfied", "output"], + "additionalProperties": false + } + }, + "required": ["url", "result"], + "additionalProperties": false + }, + "PrerequisitesApi": { + "type": "object", + "properties": { + "GET": { + "type": "object", + "properties": { + "/prerequisites": { + "$ref": "#/definitions/GetCollectionEndpoint%3CPrerequisite%3E" + }, + "/prerequisites/:id": { + "$ref": "#/definitions/GetEntityEndpoint%3CPrerequisite%2C%22id%22%3E" + } + }, + "required": ["/prerequisites", "/prerequisites/:id"], + "additionalProperties": false + }, + "POST": { + "type": "object", + "properties": { + "/prerequisites": { + "$ref": "#/definitions/PostPrerequisiteEndpoint" + }, + "/prerequisites/:id/check": { + "$ref": "#/definitions/CheckPrerequisiteEndpoint" + } + }, + "required": ["/prerequisites", "/prerequisites/:id/check"], + "additionalProperties": false + }, + "PATCH": { + "type": "object", + "properties": { + "/prerequisites/:id": { + "$ref": "#/definitions/PatchPrerequisiteEndpoint" + } + }, + "required": ["/prerequisites/:id"], + "additionalProperties": false + }, + "PUT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "DELETE": { + "type": "object", + "properties": { + "/prerequisites/:id": { + "$ref": "#/definitions/DeleteEndpoint%3CPrerequisite%2C%22id%22%3E" + } + }, + "required": ["/prerequisites/:id"], + "additionalProperties": false + }, + "HEAD": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "CONNECT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "TRACE": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "OPTIONS": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + } + }, + "required": ["GET", "POST", "PATCH", "DELETE"], + "additionalProperties": false + }, + "GetCollectionEndpoint": { + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "findOptions": { + "$ref": "#/definitions/FindOptions%3CPrerequisite%2C(%22id%22%7C%22stackName%22%7C%22name%22%7C%22type%22%7C%22config%22%7C%22installationHelp%22%7C%22createdAt%22%7C%22updatedAt%22)%5B%5D%3E" + } + }, + "additionalProperties": false + }, + "result": { + "$ref": "#/definitions/GetCollectionResult%3CPrerequisite%3E" + } + }, + "required": ["query", "result"], + "additionalProperties": false, + "description": "Rest endpoint model for getting / querying collections" + }, + "FindOptions": { + "type": "object", + "properties": { + "top": { + "type": "number", + "description": "Limits the hits" + }, + "skip": { + "type": "number", + "description": "Skips the first N hit" + }, + "order": { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "stackName": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "name": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "type": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "config": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "installationHelp": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "createdAt": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "updatedAt": { + "type": "string", + "enum": ["ASC", "DESC"] + } + }, + "additionalProperties": false, + "description": "Sets up an order by a field and a direction" + }, + "select": { + "type": "array", + "items": { + "type": "string", + "enum": ["id", "stackName", "name", "type", "config", "installationHelp", "createdAt", "updatedAt"] + }, + "description": "The result set will be limited to these fields" + }, + "filter": { + "$ref": "#/definitions/FilterType%3CPrerequisite%3E", + "description": "The fields should match this filter" + } + }, + "additionalProperties": false, + "description": "Type for default filtering model" + }, + "FilterType": { + "type": "object", + "additionalProperties": false, + "properties": { + "$and": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CPrerequisite%3E" + } + }, + "$not": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CPrerequisite%3E" + } + }, + "$nor": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CPrerequisite%3E" + } + }, + "$or": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CPrerequisite%3E" + } + }, + "id": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "UUID primary key" + }, + "$endsWith": { + "type": "string", + "description": "UUID primary key" + }, + "$like": { + "type": "string", + "description": "UUID primary key" + }, + "$regex": { + "type": "string", + "description": "UUID primary key" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "UUID primary key" + }, + "$ne": { + "type": "string", + "description": "UUID primary key" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "UUID primary key" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "UUID primary key" + } + } + }, + "additionalProperties": false + } + ] + }, + "stackName": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$endsWith": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$like": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$regex": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$ne": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + } + }, + "additionalProperties": false + } + ] + }, + "name": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + }, + "$endsWith": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + }, + "$like": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + }, + "$regex": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + }, + "$ne": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + } + } + }, + "additionalProperties": false + } + ] + }, + "type": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + }, + "$endsWith": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + }, + "$like": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + }, + "$regex": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + }, + "$ne": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + } + }, + "$nin": { + "type": "array", + "items": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + } + } + }, + "additionalProperties": false + } + ] + }, + "config": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "$ref": "#/definitions/PrerequisiteConfig", + "description": "Type-specific configuration (stored as JSON TEXT in the database)" + }, + "$ne": { + "$ref": "#/definitions/PrerequisiteConfig", + "description": "Type-specific configuration (stored as JSON TEXT in the database)" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "$ref": "#/definitions/PrerequisiteConfig", + "description": "Type-specific configuration (stored as JSON TEXT in the database)" + } + }, + "$nin": { + "type": "array", + "items": { + "$ref": "#/definitions/PrerequisiteConfig", + "description": "Type-specific configuration (stored as JSON TEXT in the database)" + } + } + }, + "additionalProperties": false + } + ] + }, + "installationHelp": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + }, + "$endsWith": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + }, + "$like": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + }, + "$regex": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + }, + "$ne": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + } + } + }, + "additionalProperties": false + } + ] + }, + "createdAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string" + }, + "$endsWith": { + "type": "string" + }, + "$like": { + "type": "string" + }, + "$regex": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + ] + }, + "updatedAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string" + }, + "$endsWith": { + "type": "string" + }, + "$like": { + "type": "string" + }, + "$regex": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + ] + } + } + }, + "GetCollectionResult": { + "type": "object", + "properties": { + "count": { + "type": "number", + "description": "The Total count of entities" + }, + "entries": { + "type": "array", + "items": { + "$ref": "#/definitions/Prerequisite" + }, + "description": "List of the selected entities" + } + }, + "required": ["count", "entries"], + "additionalProperties": false, + "description": "Response Model for GetCollection" + }, + "GetEntityEndpoint": { + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "select": { + "type": "array", + "items": { + "type": "string", + "enum": ["id", "stackName", "name", "type", "config", "installationHelp", "createdAt", "updatedAt"] + }, + "description": "The list of fields to select" + } + }, + "additionalProperties": false + }, + "url": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The entity's unique identifier" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "$ref": "#/definitions/Prerequisite" + } + }, + "required": ["query", "url", "result"], + "additionalProperties": false, + "description": "Endpoint model for getting a single entity" + }, + "DeleteEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object" + } + }, + "required": ["url", "result"], + "additionalProperties": false, + "description": "Endpoint model for deleting entities" + } + } +} diff --git a/common/schemas/services-api.json b/common/schemas/services-api.json new file mode 100644 index 0000000..348f78a --- /dev/null +++ b/common/schemas/services-api.json @@ -0,0 +1,2866 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "ServiceDefinitionWritableFields": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this service does" + }, + "workingDirectory": { + "type": "string", + "description": "Optional relative path within stack for grouping (e.g. \"frontends/public\")" + }, + "repositoryId": { + "type": "string", + "description": "Optional FK to {@link GitHubRepository.id }" + }, + "prerequisiteIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of {@link Prerequisite } entities required by this service" + }, + "prerequisiteServiceIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of other {@link ServiceDefinition } entities that must be running first" + }, + "installCommand": { + "type": "string", + "description": "Shell command to install dependencies (e.g. \"npm install\")" + }, + "buildCommand": { + "type": "string", + "description": "Shell command to build the service (e.g. \"npm run build\")" + }, + "runCommand": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceFile" + }, + "description": "Shared files placed relative to the service root" + } + }, + "required": [ + "id", + "stackName", + "displayName", + "description", + "prerequisiteIds", + "prerequisiteServiceIds", + "runCommand", + "files" + ], + "additionalProperties": false + }, + "ServiceFile": { + "type": "object", + "properties": { + "relativePath": { + "type": "string", + "description": "Path relative to the service working directory" + }, + "content": { + "type": "string", + "description": "File content (usually plain text)" + } + }, + "required": ["relativePath", "content"], + "additionalProperties": false, + "description": "A file to be placed relative to the service root (e.g. .env, appConfig.local.json)" + }, + "ServiceConfigWritableFields": { + "type": "object", + "properties": { + "serviceId": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + }, + "autoFetchEnabled": { + "type": "boolean", + "description": "Whether automatic git fetch is enabled" + }, + "autoFetchIntervalMinutes": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + }, + "autoRestartOnFetch": { + "type": "boolean", + "description": "Whether to automatically restart the service when new commits are fetched" + }, + "environmentVariableOverrides": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Per-service environment variable overrides, keyed by variable name. Overrides stack-level defaults." + } + }, + "required": [ + "serviceId", + "autoFetchEnabled", + "autoFetchIntervalMinutes", + "autoRestartOnFetch", + "environmentVariableOverrides" + ], + "additionalProperties": false + }, + "EnvironmentVariableValue": { + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": ["inherit", "custom"], + "description": "Whether to inherit the value from the host system or use a custom value" + }, + "customValue": { + "type": "string", + "description": "The custom value to use when source is 'custom'" + } + }, + "required": ["source"], + "additionalProperties": false, + "description": "Describes how an environment variable's value is resolved. Used in {@link StackConfig } for stack-level defaults and in {@link ServiceConfig } for per-service overrides." + }, + "ServiceWritableFields": { + "type": "object", + "additionalProperties": false, + "properties": { + "autoFetchEnabled": { + "type": "boolean", + "description": "Whether automatic git fetch is enabled" + }, + "autoFetchIntervalMinutes": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + }, + "autoRestartOnFetch": { + "type": "boolean", + "description": "Whether to automatically restart the service when new commits are fetched" + }, + "environmentVariableOverrides": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Per-service environment variable overrides, keyed by variable name. Overrides stack-level defaults." + }, + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this service does" + }, + "workingDirectory": { + "type": "string", + "description": "Optional relative path within stack for grouping (e.g. \"frontends/public\")" + }, + "repositoryId": { + "type": "string", + "description": "Optional FK to {@link GitHubRepository.id }" + }, + "prerequisiteIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of {@link Prerequisite } entities required by this service" + }, + "prerequisiteServiceIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of other {@link ServiceDefinition } entities that must be running first" + }, + "installCommand": { + "type": "string", + "description": "Shell command to install dependencies (e.g. \"npm install\")" + }, + "buildCommand": { + "type": "string", + "description": "Shell command to build the service (e.g. \"npm run build\")" + }, + "runCommand": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceFile" + }, + "description": "Shared files placed relative to the service root" + } + }, + "required": [ + "autoFetchEnabled", + "autoFetchIntervalMinutes", + "autoRestartOnFetch", + "description", + "displayName", + "environmentVariableOverrides", + "files", + "id", + "prerequisiteIds", + "prerequisiteServiceIds", + "runCommand", + "stackName" + ] + }, + "PostServiceEndpoint": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/ServiceView" + }, + "body": { + "$ref": "#/definitions/WithOptionalId%3CServiceWritableFields%2C%22id%22%3E" + } + }, + "required": ["result", "body"], + "additionalProperties": false + }, + "ServiceView": { + "type": "object", + "additionalProperties": false, + "properties": { + "serviceId": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + }, + "cloneStatus": { + "$ref": "#/definitions/CloneStatus" + }, + "installStatus": { + "$ref": "#/definitions/InstallStatus" + }, + "buildStatus": { + "$ref": "#/definitions/BuildStatus" + }, + "runStatus": { + "$ref": "#/definitions/RunStatus" + }, + "lastClonedAt": { + "type": "string" + }, + "lastInstalledAt": { + "type": "string" + }, + "lastBuiltAt": { + "type": "string" + }, + "lastStartedAt": { + "type": "string" + }, + "lastFetchedAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "autoFetchEnabled": { + "type": "boolean", + "description": "Whether automatic git fetch is enabled" + }, + "autoFetchIntervalMinutes": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + }, + "autoRestartOnFetch": { + "type": "boolean", + "description": "Whether to automatically restart the service when new commits are fetched" + }, + "environmentVariableOverrides": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Per-service environment variable overrides, keyed by variable name. Overrides stack-level defaults." + }, + "createdAt": { + "type": "string" + }, + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this service does" + }, + "workingDirectory": { + "type": "string", + "description": "Optional relative path within stack for grouping (e.g. \"frontends/public\")" + }, + "repositoryId": { + "type": "string", + "description": "Optional FK to {@link GitHubRepository.id }" + }, + "prerequisiteIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of {@link Prerequisite } entities required by this service" + }, + "prerequisiteServiceIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of other {@link ServiceDefinition } entities that must be running first" + }, + "installCommand": { + "type": "string", + "description": "Shell command to install dependencies (e.g. \"npm install\")" + }, + "buildCommand": { + "type": "string", + "description": "Shell command to build the service (e.g. \"npm run build\")" + }, + "runCommand": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceFile" + }, + "description": "Shared files placed relative to the service root" + } + }, + "required": [ + "autoFetchEnabled", + "autoFetchIntervalMinutes", + "autoRestartOnFetch", + "buildStatus", + "cloneStatus", + "createdAt", + "description", + "displayName", + "environmentVariableOverrides", + "files", + "id", + "installStatus", + "prerequisiteIds", + "prerequisiteServiceIds", + "runCommand", + "runStatus", + "serviceId", + "stackName", + "updatedAt" + ], + "description": "Full service view combining definition, config, and status for API responses" + }, + "CloneStatus": { + "type": "string", + "enum": ["not-cloned", "cloning", "cloned", "failed"] + }, + "InstallStatus": { + "type": "string", + "enum": ["not-installed", "installing", "installed", "failed"] + }, + "BuildStatus": { + "type": "string", + "enum": ["not-built", "building", "built", "failed"] + }, + "RunStatus": { + "type": "string", + "enum": ["stopped", "starting", "running", "stopping", "error"] + }, + "WithOptionalId": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this service does" + }, + "workingDirectory": { + "type": "string", + "description": "Optional relative path within stack for grouping (e.g. \"frontends/public\")" + }, + "repositoryId": { + "type": "string", + "description": "Optional FK to {@link GitHubRepository.id }" + }, + "prerequisiteIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of {@link Prerequisite } entities required by this service" + }, + "prerequisiteServiceIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of other {@link ServiceDefinition } entities that must be running first" + }, + "installCommand": { + "type": "string", + "description": "Shell command to install dependencies (e.g. \"npm install\")" + }, + "buildCommand": { + "type": "string", + "description": "Shell command to build the service (e.g. \"npm run build\")" + }, + "runCommand": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceFile" + }, + "description": "Shared files placed relative to the service root" + }, + "autoFetchEnabled": { + "type": "boolean", + "description": "Whether automatic git fetch is enabled" + }, + "autoFetchIntervalMinutes": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + }, + "autoRestartOnFetch": { + "type": "boolean", + "description": "Whether to automatically restart the service when new commits are fetched" + }, + "environmentVariableOverrides": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Per-service environment variable overrides, keyed by variable name. Overrides stack-level defaults." + } + }, + "required": [ + "autoFetchEnabled", + "autoFetchIntervalMinutes", + "autoRestartOnFetch", + "description", + "displayName", + "environmentVariableOverrides", + "files", + "prerequisiteIds", + "prerequisiteServiceIds", + "runCommand", + "stackName" + ] + }, + "PatchServiceEndpoint": { + "$ref": "#/definitions/PatchEndpoint%3CServiceWritableFields%2C%22id%22%3E" + }, + "PatchEndpoint": { + "type": "object", + "properties": { + "body": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this service does" + }, + "workingDirectory": { + "type": "string", + "description": "Optional relative path within stack for grouping (e.g. \"frontends/public\")" + }, + "repositoryId": { + "type": "string", + "description": "Optional FK to {@link GitHubRepository.id }" + }, + "prerequisiteIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of {@link Prerequisite } entities required by this service" + }, + "prerequisiteServiceIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of other {@link ServiceDefinition } entities that must be running first" + }, + "installCommand": { + "type": "string", + "description": "Shell command to install dependencies (e.g. \"npm install\")" + }, + "buildCommand": { + "type": "string", + "description": "Shell command to build the service (e.g. \"npm run build\")" + }, + "runCommand": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceFile" + }, + "description": "Shared files placed relative to the service root" + }, + "autoFetchEnabled": { + "type": "boolean", + "description": "Whether automatic git fetch is enabled" + }, + "autoFetchIntervalMinutes": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + }, + "autoRestartOnFetch": { + "type": "boolean", + "description": "Whether to automatically restart the service when new commits are fetched" + }, + "environmentVariableOverrides": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Per-service environment variable overrides, keyed by variable name. Overrides stack-level defaults." + } + }, + "additionalProperties": false + }, + "url": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object" + } + }, + "required": ["body", "url", "result"], + "additionalProperties": false, + "description": "Endpoint model for updating entities" + }, + "ServiceActionEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "serviceId": { + "type": "string" + } + }, + "required": ["success", "serviceId"], + "additionalProperties": false + } + }, + "required": ["url", "result"], + "additionalProperties": false + }, + "ApplyServiceFilesEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "body": { + "type": "object", + "properties": { + "relativePath": { + "type": "string" + } + }, + "additionalProperties": false + }, + "result": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + }, + "serviceId": { + "type": "string" + }, + "applied": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["success", "serviceId", "applied"], + "additionalProperties": false + } + }, + "required": ["url", "body", "result"], + "additionalProperties": false + }, + "ServiceLogsEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "query": { + "type": "object", + "properties": { + "lines": { + "type": "number" + }, + "processUid": { + "type": "string" + }, + "search": { + "type": "string" + } + }, + "additionalProperties": false + }, + "result": { + "type": "object", + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceLogEntry" + } + } + }, + "required": ["entries"], + "additionalProperties": false + } + }, + "required": ["url", "query", "result"], + "additionalProperties": false + }, + "ServiceLogEntry": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "serviceId": { + "type": "string" + }, + "processUid": { + "type": "string" + }, + "stream": { + "type": "string", + "enum": ["stdout", "stderr"] + }, + "line": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "required": ["id", "serviceId", "processUid", "stream", "line", "createdAt"], + "additionalProperties": false + }, + "ClearServiceLogsEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"], + "additionalProperties": false + } + }, + "required": ["url", "result"], + "additionalProperties": false + }, + "ServiceHistoryEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "query": { + "type": "object", + "properties": { + "limit": { + "type": "number" + } + }, + "additionalProperties": false + }, + "result": { + "type": "object", + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceStateHistory" + } + } + }, + "required": ["entries"], + "additionalProperties": false + } + }, + "required": ["url", "query", "result"], + "additionalProperties": false + }, + "ServiceStateHistory": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Auto-increment primary key" + }, + "serviceId": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + }, + "event": { + "$ref": "#/definitions/ServiceStateEvent", + "description": "The lifecycle event that occurred" + }, + "previousState": { + "type": "string", + "description": "JSON snapshot of relevant status fields before the change" + }, + "newState": { + "type": "string", + "description": "JSON snapshot of relevant status fields after the change" + }, + "triggeredBy": { + "type": "string", + "description": "Username of the user who triggered the action, or 'system'" + }, + "triggerSource": { + "$ref": "#/definitions/TriggerSource", + "description": "How the action was triggered" + }, + "metadata": { + "type": "string", + "description": "Optional JSON with extra context (exit code, error message, etc.)" + }, + "processUid": { + "type": "string", + "description": "UUID of the associated process, if this event produced log output" + }, + "createdAt": { + "type": "string" + } + }, + "required": ["id", "serviceId", "event", "triggeredBy", "triggerSource", "createdAt"], + "additionalProperties": false, + "description": "Audit log entry for service state transitions. Records every lifecycle event (start, stop, crash, install, build, pull) with full context: who triggered it, how, and any relevant metadata. Entries are never deleted and not included in exports." + }, + "ServiceStateEvent": { + "type": "string", + "enum": [ + "clone-started", + "clone-completed", + "clone-failed", + "run-started", + "run-stopped", + "run-crashed", + "run-restarted", + "install-started", + "install-completed", + "install-failed", + "build-started", + "build-completed", + "build-failed", + "setup-started", + "setup-completed", + "setup-failed", + "update-started", + "update-completed", + "update-failed", + "pull-completed", + "imported", + "state-reconciled" + ], + "description": "Event types for service state transitions. Each value represents a discrete lifecycle event that can occur." + }, + "TriggerSource": { + "type": "string", + "enum": ["api", "mcp", "auto-fetch", "auto-restart", "system"], + "description": "How a state change was triggered. Used to distinguish user actions from automated system behavior." + }, + "ServicesApi": { + "type": "object", + "properties": { + "GET": { + "type": "object", + "properties": { + "/services": { + "$ref": "#/definitions/GetCollectionEndpoint%3CServiceView%3E" + }, + "/services/:id": { + "$ref": "#/definitions/GetEntityEndpoint%3CServiceView%2C%22id%22%3E" + }, + "/services/:id/logs": { + "$ref": "#/definitions/ServiceLogsEndpoint" + }, + "/services/:id/history": { + "$ref": "#/definitions/ServiceHistoryEndpoint" + } + }, + "required": ["/services", "/services/:id", "/services/:id/logs", "/services/:id/history"], + "additionalProperties": false + }, + "POST": { + "type": "object", + "properties": { + "/services": { + "$ref": "#/definitions/PostServiceEndpoint" + }, + "/services/:id/start": { + "$ref": "#/definitions/ServiceActionEndpoint" + }, + "/services/:id/stop": { + "$ref": "#/definitions/ServiceActionEndpoint" + }, + "/services/:id/restart": { + "$ref": "#/definitions/ServiceActionEndpoint" + }, + "/services/:id/install": { + "$ref": "#/definitions/ServiceActionEndpoint" + }, + "/services/:id/build": { + "$ref": "#/definitions/ServiceActionEndpoint" + }, + "/services/:id/pull": { + "$ref": "#/definitions/ServiceActionEndpoint" + }, + "/services/:id/setup": { + "$ref": "#/definitions/ServiceActionEndpoint" + }, + "/services/:id/update": { + "$ref": "#/definitions/ServiceActionEndpoint" + }, + "/services/:id/apply-files": { + "$ref": "#/definitions/ApplyServiceFilesEndpoint" + } + }, + "required": [ + "/services", + "/services/:id/start", + "/services/:id/stop", + "/services/:id/restart", + "/services/:id/install", + "/services/:id/build", + "/services/:id/pull", + "/services/:id/setup", + "/services/:id/update", + "/services/:id/apply-files" + ], + "additionalProperties": false + }, + "PATCH": { + "type": "object", + "properties": { + "/services/:id": { + "$ref": "#/definitions/PatchServiceEndpoint" + } + }, + "required": ["/services/:id"], + "additionalProperties": false + }, + "PUT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "DELETE": { + "type": "object", + "properties": { + "/services/:id": { + "$ref": "#/definitions/DeleteEndpoint%3CServiceDefinition%2C%22id%22%3E" + }, + "/services/:id/logs": { + "$ref": "#/definitions/ClearServiceLogsEndpoint" + } + }, + "required": ["/services/:id", "/services/:id/logs"], + "additionalProperties": false + }, + "HEAD": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "CONNECT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "TRACE": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "OPTIONS": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + } + }, + "required": ["GET", "POST", "PATCH", "DELETE"], + "additionalProperties": false + }, + "GetCollectionEndpoint": { + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "findOptions": { + "$ref": "#/definitions/FindOptions%3CServiceView%2C(%22id%22%7C%22stackName%22%7C%22displayName%22%7C%22description%22%7C%22workingDirectory%22%7C%22repositoryId%22%7C%22prerequisiteIds%22%7C%22prerequisiteServiceIds%22%7C%22installCommand%22%7C%22buildCommand%22%7C%22runCommand%22%7C%22files%22%7C%22createdAt%22%7C%22updatedAt%22%7C%22serviceId%22%7C%22autoFetchEnabled%22%7C%22autoFetchIntervalMinutes%22%7C%22autoRestartOnFetch%22%7C%22environmentVariableOverrides%22%7C%22cloneStatus%22%7C%22installStatus%22%7C%22buildStatus%22%7C%22runStatus%22%7C%22lastClonedAt%22%7C%22lastInstalledAt%22%7C%22lastBuiltAt%22%7C%22lastStartedAt%22%7C%22lastFetchedAt%22)%5B%5D%3E" + } + }, + "additionalProperties": false + }, + "result": { + "$ref": "#/definitions/GetCollectionResult%3CServiceView%3E" + } + }, + "required": ["query", "result"], + "additionalProperties": false, + "description": "Rest endpoint model for getting / querying collections" + }, + "FindOptions": { + "type": "object", + "properties": { + "top": { + "type": "number", + "description": "Limits the hits" + }, + "skip": { + "type": "number", + "description": "Skips the first N hit" + }, + "order": { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "stackName": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "displayName": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "description": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "workingDirectory": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "repositoryId": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "prerequisiteIds": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "prerequisiteServiceIds": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "installCommand": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "buildCommand": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "runCommand": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "files": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "createdAt": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "updatedAt": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "serviceId": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "autoFetchEnabled": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "autoFetchIntervalMinutes": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "autoRestartOnFetch": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "environmentVariableOverrides": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "cloneStatus": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "installStatus": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "buildStatus": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "runStatus": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "lastClonedAt": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "lastInstalledAt": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "lastBuiltAt": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "lastStartedAt": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "lastFetchedAt": { + "type": "string", + "enum": ["ASC", "DESC"] + } + }, + "additionalProperties": false, + "description": "Sets up an order by a field and a direction" + }, + "select": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "id", + "stackName", + "displayName", + "description", + "workingDirectory", + "repositoryId", + "prerequisiteIds", + "prerequisiteServiceIds", + "installCommand", + "buildCommand", + "runCommand", + "files", + "createdAt", + "updatedAt", + "serviceId", + "autoFetchEnabled", + "autoFetchIntervalMinutes", + "autoRestartOnFetch", + "environmentVariableOverrides", + "cloneStatus", + "installStatus", + "buildStatus", + "runStatus", + "lastClonedAt", + "lastInstalledAt", + "lastBuiltAt", + "lastStartedAt", + "lastFetchedAt" + ] + }, + "description": "The result set will be limited to these fields" + }, + "filter": { + "$ref": "#/definitions/FilterType%3CServiceView%3E", + "description": "The fields should match this filter" + } + }, + "additionalProperties": false, + "description": "Type for default filtering model" + }, + "FilterType": { + "type": "object", + "additionalProperties": false, + "properties": { + "$and": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CServiceView%3E" + } + }, + "$not": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CServiceView%3E" + } + }, + "$nor": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CServiceView%3E" + } + }, + "$or": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CServiceView%3E" + } + }, + "id": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "UUID primary key" + }, + "$endsWith": { + "type": "string", + "description": "UUID primary key" + }, + "$like": { + "type": "string", + "description": "UUID primary key" + }, + "$regex": { + "type": "string", + "description": "UUID primary key" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "UUID primary key" + }, + "$ne": { + "type": "string", + "description": "UUID primary key" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "UUID primary key" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "UUID primary key" + } + } + }, + "additionalProperties": false + } + ] + }, + "stackName": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$endsWith": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$like": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$regex": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$ne": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + } + }, + "additionalProperties": false + } + ] + }, + "displayName": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "$endsWith": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "$like": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "$regex": { + "type": "string", + "description": "Human-readable name shown in the UI" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "$ne": { + "type": "string", + "description": "Human-readable name shown in the UI" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "Human-readable name shown in the UI" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "Human-readable name shown in the UI" + } + } + }, + "additionalProperties": false + } + ] + }, + "description": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "Optional description of what this service does" + }, + "$endsWith": { + "type": "string", + "description": "Optional description of what this service does" + }, + "$like": { + "type": "string", + "description": "Optional description of what this service does" + }, + "$regex": { + "type": "string", + "description": "Optional description of what this service does" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Optional description of what this service does" + }, + "$ne": { + "type": "string", + "description": "Optional description of what this service does" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "Optional description of what this service does" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "Optional description of what this service does" + } + } + }, + "additionalProperties": false + } + ] + }, + "workingDirectory": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Optional relative path within stack for grouping (e.g. \"frontends/public\")" + }, + "$ne": { + "type": "string", + "description": "Optional relative path within stack for grouping (e.g. \"frontends/public\")" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ], + "description": "Optional relative path within stack for grouping (e.g. \"frontends/public\")" + } + }, + "$nin": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ], + "description": "Optional relative path within stack for grouping (e.g. \"frontends/public\")" + } + } + }, + "additionalProperties": false + } + ] + }, + "repositoryId": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Optional FK to {@link GitHubRepository.id }" + }, + "$ne": { + "type": "string", + "description": "Optional FK to {@link GitHubRepository.id }" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ], + "description": "Optional FK to {@link GitHubRepository.id }" + } + }, + "$nin": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ], + "description": "Optional FK to {@link GitHubRepository.id }" + } + } + }, + "additionalProperties": false + } + ] + }, + "prerequisiteIds": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of {@link Prerequisite } entities required by this service" + }, + "$ne": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of {@link Prerequisite } entities required by this service" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of {@link Prerequisite } entities required by this service" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of {@link Prerequisite } entities required by this service" + } + } + }, + "additionalProperties": false + } + ] + }, + "prerequisiteServiceIds": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of other {@link ServiceDefinition } entities that must be running first" + }, + "$ne": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of other {@link ServiceDefinition } entities that must be running first" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of other {@link ServiceDefinition } entities that must be running first" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of other {@link ServiceDefinition } entities that must be running first" + } + } + }, + "additionalProperties": false + } + ] + }, + "installCommand": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Shell command to install dependencies (e.g. \"npm install\")" + }, + "$ne": { + "type": "string", + "description": "Shell command to install dependencies (e.g. \"npm install\")" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ], + "description": "Shell command to install dependencies (e.g. \"npm install\")" + } + }, + "$nin": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ], + "description": "Shell command to install dependencies (e.g. \"npm install\")" + } + } + }, + "additionalProperties": false + } + ] + }, + "buildCommand": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Shell command to build the service (e.g. \"npm run build\")" + }, + "$ne": { + "type": "string", + "description": "Shell command to build the service (e.g. \"npm run build\")" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ], + "description": "Shell command to build the service (e.g. \"npm run build\")" + } + }, + "$nin": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ], + "description": "Shell command to build the service (e.g. \"npm run build\")" + } + } + }, + "additionalProperties": false + } + ] + }, + "runCommand": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + }, + "$endsWith": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + }, + "$like": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + }, + "$regex": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + }, + "$ne": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + } + } + }, + "additionalProperties": false + } + ] + }, + "files": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceFile" + }, + "description": "Shared files placed relative to the service root" + }, + "$ne": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceFile" + }, + "description": "Shared files placed relative to the service root" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceFile" + }, + "description": "Shared files placed relative to the service root" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceFile" + }, + "description": "Shared files placed relative to the service root" + } + } + }, + "additionalProperties": false + } + ] + }, + "createdAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string" + }, + "$endsWith": { + "type": "string" + }, + "$like": { + "type": "string" + }, + "$regex": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + ] + }, + "updatedAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string" + }, + "$endsWith": { + "type": "string" + }, + "$like": { + "type": "string" + }, + "$regex": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + ] + }, + "serviceId": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + }, + "$endsWith": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + }, + "$like": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + }, + "$regex": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + }, + "$ne": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "FK to {@link ServiceDefinition.id }" + } + } + }, + "additionalProperties": false + } + ] + }, + "autoFetchEnabled": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "boolean", + "description": "Whether automatic git fetch is enabled" + }, + "$ne": { + "type": "boolean", + "description": "Whether automatic git fetch is enabled" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "boolean", + "description": "Whether automatic git fetch is enabled" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "boolean", + "description": "Whether automatic git fetch is enabled" + } + } + }, + "additionalProperties": false + } + ] + }, + "autoFetchIntervalMinutes": { + "anyOf": [ + { + "type": "object", + "properties": { + "$gt": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + }, + "$gte": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + }, + "$lt": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + }, + "$lte": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + }, + "$ne": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + } + } + }, + "additionalProperties": false + } + ] + }, + "autoRestartOnFetch": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "boolean", + "description": "Whether to automatically restart the service when new commits are fetched" + }, + "$ne": { + "type": "boolean", + "description": "Whether to automatically restart the service when new commits are fetched" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "boolean", + "description": "Whether to automatically restart the service when new commits are fetched" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "boolean", + "description": "Whether to automatically restart the service when new commits are fetched" + } + } + }, + "additionalProperties": false + } + ] + }, + "environmentVariableOverrides": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Per-service environment variable overrides, keyed by variable name. Overrides stack-level defaults." + }, + "$ne": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Per-service environment variable overrides, keyed by variable name. Overrides stack-level defaults." + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Per-service environment variable overrides, keyed by variable name. Overrides stack-level defaults." + } + }, + "$nin": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Per-service environment variable overrides, keyed by variable name. Overrides stack-level defaults." + } + } + }, + "additionalProperties": false + } + ] + }, + "cloneStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "$ref": "#/definitions/CloneStatus" + }, + "$endsWith": { + "$ref": "#/definitions/CloneStatus" + }, + "$like": { + "$ref": "#/definitions/CloneStatus" + }, + "$regex": { + "$ref": "#/definitions/CloneStatus" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "$ref": "#/definitions/CloneStatus" + }, + "$ne": { + "$ref": "#/definitions/CloneStatus" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "$ref": "#/definitions/CloneStatus" + } + }, + "$nin": { + "type": "array", + "items": { + "$ref": "#/definitions/CloneStatus" + } + } + }, + "additionalProperties": false + } + ] + }, + "installStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "$ref": "#/definitions/InstallStatus" + }, + "$endsWith": { + "$ref": "#/definitions/InstallStatus" + }, + "$like": { + "$ref": "#/definitions/InstallStatus" + }, + "$regex": { + "$ref": "#/definitions/InstallStatus" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "$ref": "#/definitions/InstallStatus" + }, + "$ne": { + "$ref": "#/definitions/InstallStatus" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "$ref": "#/definitions/InstallStatus" + } + }, + "$nin": { + "type": "array", + "items": { + "$ref": "#/definitions/InstallStatus" + } + } + }, + "additionalProperties": false + } + ] + }, + "buildStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "$ref": "#/definitions/BuildStatus" + }, + "$endsWith": { + "$ref": "#/definitions/BuildStatus" + }, + "$like": { + "$ref": "#/definitions/BuildStatus" + }, + "$regex": { + "$ref": "#/definitions/BuildStatus" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "$ref": "#/definitions/BuildStatus" + }, + "$ne": { + "$ref": "#/definitions/BuildStatus" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "$ref": "#/definitions/BuildStatus" + } + }, + "$nin": { + "type": "array", + "items": { + "$ref": "#/definitions/BuildStatus" + } + } + }, + "additionalProperties": false + } + ] + }, + "runStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "$ref": "#/definitions/RunStatus" + }, + "$endsWith": { + "$ref": "#/definitions/RunStatus" + }, + "$like": { + "$ref": "#/definitions/RunStatus" + }, + "$regex": { + "$ref": "#/definitions/RunStatus" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "$ref": "#/definitions/RunStatus" + }, + "$ne": { + "$ref": "#/definitions/RunStatus" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "$ref": "#/definitions/RunStatus" + } + }, + "$nin": { + "type": "array", + "items": { + "$ref": "#/definitions/RunStatus" + } + } + }, + "additionalProperties": false + } + ] + }, + "lastClonedAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ] + } + }, + "$nin": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ] + } + } + }, + "additionalProperties": false + } + ] + }, + "lastInstalledAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ] + } + }, + "$nin": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ] + } + } + }, + "additionalProperties": false + } + ] + }, + "lastBuiltAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ] + } + }, + "$nin": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ] + } + } + }, + "additionalProperties": false + } + ] + }, + "lastStartedAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ] + } + }, + "$nin": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ] + } + } + }, + "additionalProperties": false + } + ] + }, + "lastFetchedAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ] + } + }, + "$nin": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ] + } + } + }, + "additionalProperties": false + } + ] + } + } + }, + "GetCollectionResult": { + "type": "object", + "properties": { + "count": { + "type": "number", + "description": "The Total count of entities" + }, + "entries": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceView" + }, + "description": "List of the selected entities" + } + }, + "required": ["count", "entries"], + "additionalProperties": false, + "description": "Response Model for GetCollection" + }, + "GetEntityEndpoint": { + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "select": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "id", + "stackName", + "displayName", + "description", + "workingDirectory", + "repositoryId", + "prerequisiteIds", + "prerequisiteServiceIds", + "installCommand", + "buildCommand", + "runCommand", + "files", + "createdAt", + "updatedAt", + "serviceId", + "autoFetchEnabled", + "autoFetchIntervalMinutes", + "autoRestartOnFetch", + "environmentVariableOverrides", + "cloneStatus", + "installStatus", + "buildStatus", + "runStatus", + "lastClonedAt", + "lastInstalledAt", + "lastBuiltAt", + "lastStartedAt", + "lastFetchedAt" + ] + }, + "description": "The list of fields to select" + } + }, + "additionalProperties": false + }, + "url": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The entity's unique identifier" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "$ref": "#/definitions/ServiceView" + } + }, + "required": ["query", "url", "result"], + "additionalProperties": false, + "description": "Endpoint model for getting a single entity" + }, + "DeleteEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object" + } + }, + "required": ["url", "result"], + "additionalProperties": false, + "description": "Endpoint model for deleting entities" + } + } +} diff --git a/common/schemas/stacks-api.json b/common/schemas/stacks-api.json new file mode 100644 index 0000000..d0fd3dc --- /dev/null +++ b/common/schemas/stacks-api.json @@ -0,0 +1,1549 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "StackWritableFields": { + "type": "object", + "additionalProperties": false, + "properties": { + "mainDirectory": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + }, + "environmentVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Stack-level environment variable values, keyed by variable name" + }, + "name": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this stack does" + } + }, + "required": ["description", "displayName", "environmentVariables", "mainDirectory", "name"] + }, + "EnvironmentVariableValue": { + "type": "object", + "properties": { + "source": { + "type": "string", + "enum": ["inherit", "custom"], + "description": "Whether to inherit the value from the host system or use a custom value" + }, + "customValue": { + "type": "string", + "description": "The custom value to use when source is 'custom'" + } + }, + "required": ["source"], + "additionalProperties": false, + "description": "Describes how an environment variable's value is resolved. Used in {@link StackConfig } for stack-level defaults and in {@link ServiceConfig } for per-service overrides." + }, + "PostStackEndpoint": { + "type": "object", + "properties": { + "result": { + "$ref": "#/definitions/StackView" + }, + "body": { + "$ref": "#/definitions/WithOptionalId%3CStackWritableFields%2C%22name%22%3E" + } + }, + "required": ["result", "body"], + "additionalProperties": false + }, + "StackView": { + "type": "object", + "additionalProperties": false, + "properties": { + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "mainDirectory": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + }, + "environmentVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Stack-level environment variable values, keyed by variable name" + }, + "createdAt": { + "type": "string" + }, + "updatedAt": { + "type": "string" + }, + "name": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this stack does" + } + }, + "required": [ + "createdAt", + "description", + "displayName", + "environmentVariables", + "mainDirectory", + "name", + "stackName", + "updatedAt" + ], + "description": "Full stack view combining definition and config for API responses" + }, + "WithOptionalId": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this stack does" + }, + "mainDirectory": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + }, + "environmentVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Stack-level environment variable values, keyed by variable name" + } + }, + "required": ["description", "displayName", "environmentVariables", "mainDirectory"] + }, + "PatchStackEndpoint": { + "$ref": "#/definitions/PatchEndpoint%3CStackWritableFields%2C%22name%22%3E" + }, + "PatchEndpoint": { + "type": "object", + "properties": { + "body": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this stack does" + }, + "mainDirectory": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + }, + "environmentVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Stack-level environment variable values, keyed by variable name" + } + }, + "additionalProperties": false + }, + "url": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object" + } + }, + "required": ["body", "url", "result"], + "additionalProperties": false, + "description": "Endpoint model for updating entities" + }, + "ExportStackEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object", + "properties": { + "stack": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this stack does" + } + }, + "required": ["name", "displayName", "description"], + "additionalProperties": false + }, + "services": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this service does" + }, + "workingDirectory": { + "type": "string", + "description": "Optional relative path within stack for grouping (e.g. \"frontends/public\")" + }, + "repositoryId": { + "type": "string", + "description": "Optional FK to {@link GitHubRepository.id }" + }, + "prerequisiteIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of {@link Prerequisite } entities required by this service" + }, + "prerequisiteServiceIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of other {@link ServiceDefinition } entities that must be running first" + }, + "installCommand": { + "type": "string", + "description": "Shell command to install dependencies (e.g. \"npm install\")" + }, + "buildCommand": { + "type": "string", + "description": "Shell command to build the service (e.g. \"npm run build\")" + }, + "runCommand": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceFile" + }, + "description": "Shared files placed relative to the service root" + } + }, + "required": [ + "id", + "stackName", + "displayName", + "description", + "prerequisiteIds", + "prerequisiteServiceIds", + "runCommand", + "files" + ], + "additionalProperties": false + } + }, + "repositories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "url": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description" + } + }, + "required": ["id", "stackName", "url", "displayName", "description"], + "additionalProperties": false + } + }, + "prerequisites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "name": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + }, + "type": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + }, + "config": { + "$ref": "#/definitions/PrerequisiteConfig", + "description": "Type-specific configuration (stored as JSON TEXT in the database)" + }, + "installationHelp": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + } + }, + "required": ["id", "stackName", "name", "type", "config", "installationHelp"], + "additionalProperties": false + } + } + }, + "required": ["stack", "services", "repositories", "prerequisites"], + "additionalProperties": false + } + }, + "required": ["url", "result"], + "additionalProperties": false + }, + "ServiceFile": { + "type": "object", + "properties": { + "relativePath": { + "type": "string", + "description": "Path relative to the service working directory" + }, + "content": { + "type": "string", + "description": "File content (usually plain text)" + } + }, + "required": ["relativePath", "content"], + "additionalProperties": false, + "description": "A file to be placed relative to the service root (e.g. .env, appConfig.local.json)" + }, + "PrerequisiteType": { + "type": "string", + "enum": [ + "node", + "yarn", + "dotnet-sdk", + "dotnet-runtime", + "nuget-feed", + "git", + "github-cli", + "env-variable", + "custom-script" + ], + "description": "The type of prerequisite that must be satisfied before a stack or service can run." + }, + "PrerequisiteConfig": { + "anyOf": [ + { + "type": "object", + "properties": { + "minimumVersion": { + "type": "string" + } + }, + "required": ["minimumVersion"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "minimumVersion": { + "type": "string" + } + }, + "required": ["minimumVersion"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "feedUrl": { + "type": "string" + }, + "feedName": { + "type": "string" + } + }, + "required": ["feedUrl"], + "additionalProperties": false + }, + { + "type": "object", + "additionalProperties": { + "not": {} + } + }, + { + "type": "object", + "properties": { + "variableName": { + "type": "string" + } + }, + "required": ["variableName"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "script": { + "type": "string" + } + }, + "required": ["script"], + "additionalProperties": false + } + ], + "description": "Union of all possible prerequisite config shapes." + }, + "ImportStackEndpoint": { + "type": "object", + "properties": { + "result": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"], + "additionalProperties": false + }, + "body": { + "type": "object", + "properties": { + "stack": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this stack does" + } + }, + "required": ["name", "displayName", "description"], + "additionalProperties": false + }, + "services": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description of what this service does" + }, + "workingDirectory": { + "type": "string", + "description": "Optional relative path within stack for grouping (e.g. \"frontends/public\")" + }, + "repositoryId": { + "type": "string", + "description": "Optional FK to {@link GitHubRepository.id }" + }, + "prerequisiteIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of {@link Prerequisite } entities required by this service" + }, + "prerequisiteServiceIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of other {@link ServiceDefinition } entities that must be running first" + }, + "installCommand": { + "type": "string", + "description": "Shell command to install dependencies (e.g. \"npm install\")" + }, + "buildCommand": { + "type": "string", + "description": "Shell command to build the service (e.g. \"npm run build\")" + }, + "runCommand": { + "type": "string", + "description": "Shell command to run the service (e.g. \"npm start\")" + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/ServiceFile" + }, + "description": "Shared files placed relative to the service root" + } + }, + "required": [ + "id", + "stackName", + "displayName", + "description", + "prerequisiteIds", + "prerequisiteServiceIds", + "runCommand", + "files" + ], + "additionalProperties": false + } + }, + "repositories": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "url": { + "type": "string", + "description": "Full URL to the git repository (e.g. \"https://github.com/user/repo\")" + }, + "displayName": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "description": { + "type": "string", + "description": "Optional description" + } + }, + "required": ["id", "stackName", "url", "displayName", "description"], + "additionalProperties": false + } + }, + "prerequisites": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "UUID primary key" + }, + "stackName": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "name": { + "type": "string", + "description": "Human-readable prerequisite name (e.g. \"Node.js >= 18\")" + }, + "type": { + "$ref": "#/definitions/PrerequisiteType", + "description": "Discriminator that determines the check logic and config shape" + }, + "config": { + "$ref": "#/definitions/PrerequisiteConfig", + "description": "Type-specific configuration (stored as JSON TEXT in the database)" + }, + "installationHelp": { + "type": "string", + "description": "Help text shown when the prerequisite check fails" + } + }, + "required": ["id", "stackName", "name", "type", "config", "installationHelp"], + "additionalProperties": false + } + }, + "config": { + "type": "object", + "properties": { + "mainDirectory": { + "type": "string" + }, + "environmentVariables": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + } + }, + "services": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "environmentVariableOverrides": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + } + }, + "autoFetchEnabled": { + "type": "boolean", + "description": "Whether automatic git fetch is enabled" + }, + "autoFetchIntervalMinutes": { + "type": "number", + "description": "Interval in minutes between automatic git fetches" + }, + "autoRestartOnFetch": { + "type": "boolean", + "description": "Whether to automatically restart the service when new commits are fetched" + } + } + } + } + }, + "required": ["mainDirectory"], + "additionalProperties": false + } + }, + "required": ["stack", "services", "repositories", "prerequisites", "config"], + "additionalProperties": false + } + }, + "required": ["result", "body"], + "additionalProperties": false + }, + "StackSetupEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"], + "additionalProperties": false + } + }, + "required": ["url", "result"], + "additionalProperties": false + }, + "StacksApi": { + "type": "object", + "properties": { + "GET": { + "type": "object", + "properties": { + "/stacks": { + "$ref": "#/definitions/GetCollectionEndpoint%3CStackView%3E" + }, + "/stacks/:id": { + "$ref": "#/definitions/GetEntityEndpoint%3CStackView%2C%22name%22%3E" + }, + "/stacks/:id/export": { + "$ref": "#/definitions/ExportStackEndpoint" + } + }, + "required": ["/stacks", "/stacks/:id", "/stacks/:id/export"], + "additionalProperties": false + }, + "POST": { + "type": "object", + "properties": { + "/stacks": { + "$ref": "#/definitions/PostStackEndpoint" + }, + "/stacks/import": { + "$ref": "#/definitions/ImportStackEndpoint" + }, + "/stacks/:id/setup": { + "$ref": "#/definitions/StackSetupEndpoint" + } + }, + "required": ["/stacks", "/stacks/import", "/stacks/:id/setup"], + "additionalProperties": false + }, + "PATCH": { + "type": "object", + "properties": { + "/stacks/:id": { + "$ref": "#/definitions/PatchStackEndpoint" + } + }, + "required": ["/stacks/:id"], + "additionalProperties": false + }, + "PUT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "DELETE": { + "type": "object", + "properties": { + "/stacks/:id": { + "$ref": "#/definitions/DeleteEndpoint%3CStackDefinition%2C%22name%22%3E" + } + }, + "required": ["/stacks/:id"], + "additionalProperties": false + }, + "HEAD": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "CONNECT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "TRACE": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "OPTIONS": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + } + }, + "required": ["GET", "POST", "PATCH", "DELETE"], + "additionalProperties": false + }, + "GetCollectionEndpoint": { + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "findOptions": { + "$ref": "#/definitions/FindOptions%3CStackView%2C(%22name%22%7C%22displayName%22%7C%22description%22%7C%22createdAt%22%7C%22updatedAt%22%7C%22stackName%22%7C%22mainDirectory%22%7C%22environmentVariables%22)%5B%5D%3E" + } + }, + "additionalProperties": false + }, + "result": { + "$ref": "#/definitions/GetCollectionResult%3CStackView%3E" + } + }, + "required": ["query", "result"], + "additionalProperties": false, + "description": "Rest endpoint model for getting / querying collections" + }, + "FindOptions": { + "type": "object", + "properties": { + "top": { + "type": "number", + "description": "Limits the hits" + }, + "skip": { + "type": "number", + "description": "Skips the first N hit" + }, + "order": { + "type": "object", + "properties": { + "name": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "displayName": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "description": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "createdAt": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "updatedAt": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "stackName": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "mainDirectory": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "environmentVariables": { + "type": "string", + "enum": ["ASC", "DESC"] + } + }, + "additionalProperties": false, + "description": "Sets up an order by a field and a direction" + }, + "select": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "name", + "displayName", + "description", + "createdAt", + "updatedAt", + "stackName", + "mainDirectory", + "environmentVariables" + ] + }, + "description": "The result set will be limited to these fields" + }, + "filter": { + "$ref": "#/definitions/FilterType%3CStackView%3E", + "description": "The fields should match this filter" + } + }, + "additionalProperties": false, + "description": "Type for default filtering model" + }, + "FilterType": { + "type": "object", + "additionalProperties": false, + "properties": { + "$and": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CStackView%3E" + } + }, + "$not": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CStackView%3E" + } + }, + "$nor": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CStackView%3E" + } + }, + "$or": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CStackView%3E" + } + }, + "name": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + }, + "$endsWith": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + }, + "$like": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + }, + "$regex": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + }, + "$ne": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + } + } + }, + "additionalProperties": false + } + ] + }, + "displayName": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "$endsWith": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "$like": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "$regex": { + "type": "string", + "description": "Human-readable name shown in the UI" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Human-readable name shown in the UI" + }, + "$ne": { + "type": "string", + "description": "Human-readable name shown in the UI" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "Human-readable name shown in the UI" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "Human-readable name shown in the UI" + } + } + }, + "additionalProperties": false + } + ] + }, + "description": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "Optional description of what this stack does" + }, + "$endsWith": { + "type": "string", + "description": "Optional description of what this stack does" + }, + "$like": { + "type": "string", + "description": "Optional description of what this stack does" + }, + "$regex": { + "type": "string", + "description": "Optional description of what this stack does" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Optional description of what this stack does" + }, + "$ne": { + "type": "string", + "description": "Optional description of what this stack does" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "Optional description of what this stack does" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "Optional description of what this stack does" + } + } + }, + "additionalProperties": false + } + ] + }, + "createdAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string" + }, + "$endsWith": { + "type": "string" + }, + "$like": { + "type": "string" + }, + "$regex": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + ] + }, + "updatedAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string" + }, + "$endsWith": { + "type": "string" + }, + "$like": { + "type": "string" + }, + "$regex": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + ] + }, + "stackName": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$endsWith": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$like": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$regex": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + }, + "$ne": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "FK to {@link StackDefinition.name }" + } + } + }, + "additionalProperties": false + } + ] + }, + "mainDirectory": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + }, + "$endsWith": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + }, + "$like": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + }, + "$regex": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + }, + "$ne": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string", + "description": "Absolute path to the root directory for all services in this stack" + } + } + }, + "additionalProperties": false + } + ] + }, + "environmentVariables": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Stack-level environment variable values, keyed by variable name" + }, + "$ne": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Stack-level environment variable values, keyed by variable name" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Stack-level environment variable values, keyed by variable name" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/EnvironmentVariableValue" + }, + "description": "Stack-level environment variable values, keyed by variable name" + } + } + }, + "additionalProperties": false + } + ] + } + } + }, + "GetCollectionResult": { + "type": "object", + "properties": { + "count": { + "type": "number", + "description": "The Total count of entities" + }, + "entries": { + "type": "array", + "items": { + "$ref": "#/definitions/StackView" + }, + "description": "List of the selected entities" + } + }, + "required": ["count", "entries"], + "additionalProperties": false, + "description": "Response Model for GetCollection" + }, + "GetEntityEndpoint": { + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "select": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "name", + "displayName", + "description", + "createdAt", + "updatedAt", + "stackName", + "mainDirectory", + "environmentVariables" + ] + }, + "description": "The list of fields to select" + } + }, + "additionalProperties": false + }, + "url": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The entity's unique identifier" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "$ref": "#/definitions/StackView" + } + }, + "required": ["query", "url", "result"], + "additionalProperties": false, + "description": "Endpoint model for getting a single entity" + }, + "DeleteEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Unique kebab-case identifier for the stack" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object" + } + }, + "required": ["url", "result"], + "additionalProperties": false, + "description": "Endpoint model for deleting entities" + } + } +} diff --git a/common/schemas/system-api.json b/common/schemas/system-api.json new file mode 100644 index 0000000..591a6b0 --- /dev/null +++ b/common/schemas/system-api.json @@ -0,0 +1,168 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "CheckEnvAvailabilityEndpoint": { + "type": "object", + "properties": { + "result": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "body": { + "type": "object", + "properties": { + "variableNames": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["variableNames"], + "additionalProperties": false + } + }, + "required": ["result", "body"], + "additionalProperties": false + }, + "SystemApi": { + "type": "object", + "properties": { + "GET": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "POST": { + "type": "object", + "properties": { + "/system/check-env-availability": { + "$ref": "#/definitions/CheckEnvAvailabilityEndpoint" + } + }, + "required": ["/system/check-env-availability"], + "additionalProperties": false + }, + "PATCH": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "PUT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "DELETE": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "HEAD": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "CONNECT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "TRACE": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "OPTIONS": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + } + }, + "required": ["POST"], + "additionalProperties": false + } + } +} diff --git a/common/schemas/tokens-api.json b/common/schemas/tokens-api.json new file mode 100644 index 0000000..b781e5f --- /dev/null +++ b/common/schemas/tokens-api.json @@ -0,0 +1,584 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "CreateTokenEndpoint": { + "type": "object", + "properties": { + "result": { + "type": "object", + "properties": { + "token": { + "$ref": "#/definitions/PublicApiToken" + }, + "plainTextToken": { + "type": "string" + } + }, + "required": ["token", "plainTextToken"], + "additionalProperties": false + }, + "body": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["result", "body"], + "additionalProperties": false + }, + "PublicApiToken": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "username": { + "type": "string" + }, + "name": { + "type": "string" + }, + "lastUsedAt": { + "type": "string" + }, + "createdAt": { + "type": "string" + } + }, + "required": ["id", "username", "name", "createdAt"], + "additionalProperties": false + }, + "TokensApi": { + "type": "object", + "properties": { + "GET": { + "type": "object", + "properties": { + "/tokens": { + "$ref": "#/definitions/GetCollectionEndpoint%3CPublicApiToken%3E" + } + }, + "required": ["/tokens"], + "additionalProperties": false + }, + "POST": { + "type": "object", + "properties": { + "/tokens": { + "$ref": "#/definitions/CreateTokenEndpoint" + } + }, + "required": ["/tokens"], + "additionalProperties": false + }, + "PATCH": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "PUT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "DELETE": { + "type": "object", + "properties": { + "/tokens/:id": { + "$ref": "#/definitions/DeleteEndpoint%3CApiToken%2C%22id%22%3E" + } + }, + "required": ["/tokens/:id"], + "additionalProperties": false + }, + "HEAD": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "CONNECT": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "TRACE": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + }, + "OPTIONS": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "result": {}, + "url": {}, + "query": {}, + "body": {}, + "headers": {} + }, + "required": ["result"], + "additionalProperties": false + } + } + }, + "required": ["GET", "POST", "DELETE"], + "additionalProperties": false + }, + "GetCollectionEndpoint": { + "type": "object", + "properties": { + "query": { + "type": "object", + "properties": { + "findOptions": { + "$ref": "#/definitions/FindOptions%3CPublicApiToken%2C(%22id%22%7C%22username%22%7C%22name%22%7C%22lastUsedAt%22%7C%22createdAt%22)%5B%5D%3E" + } + }, + "additionalProperties": false + }, + "result": { + "$ref": "#/definitions/GetCollectionResult%3CPublicApiToken%3E" + } + }, + "required": ["query", "result"], + "additionalProperties": false, + "description": "Rest endpoint model for getting / querying collections" + }, + "FindOptions": { + "type": "object", + "properties": { + "top": { + "type": "number", + "description": "Limits the hits" + }, + "skip": { + "type": "number", + "description": "Skips the first N hit" + }, + "order": { + "type": "object", + "properties": { + "id": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "username": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "name": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "lastUsedAt": { + "type": "string", + "enum": ["ASC", "DESC"] + }, + "createdAt": { + "type": "string", + "enum": ["ASC", "DESC"] + } + }, + "additionalProperties": false, + "description": "Sets up an order by a field and a direction" + }, + "select": { + "type": "array", + "items": { + "type": "string", + "enum": ["id", "username", "name", "lastUsedAt", "createdAt"] + }, + "description": "The result set will be limited to these fields" + }, + "filter": { + "$ref": "#/definitions/FilterType%3CPublicApiToken%3E", + "description": "The fields should match this filter" + } + }, + "additionalProperties": false, + "description": "Type for default filtering model" + }, + "FilterType": { + "type": "object", + "additionalProperties": false, + "properties": { + "$and": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CPublicApiToken%3E" + } + }, + "$not": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CPublicApiToken%3E" + } + }, + "$nor": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CPublicApiToken%3E" + } + }, + "$or": { + "type": "array", + "items": { + "$ref": "#/definitions/FilterType%3CPublicApiToken%3E" + } + }, + "id": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string" + }, + "$endsWith": { + "type": "string" + }, + "$like": { + "type": "string" + }, + "$regex": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + ] + }, + "username": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string" + }, + "$endsWith": { + "type": "string" + }, + "$like": { + "type": "string" + }, + "$regex": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + ] + }, + "name": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string" + }, + "$endsWith": { + "type": "string" + }, + "$like": { + "type": "string" + }, + "$regex": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + ] + }, + "lastUsedAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ] + } + }, + "$nin": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "not": {} + } + ] + } + } + }, + "additionalProperties": false + } + ] + }, + "createdAt": { + "anyOf": [ + { + "type": "object", + "properties": { + "$startsWith": { + "type": "string" + }, + "$endsWith": { + "type": "string" + }, + "$like": { + "type": "string" + }, + "$regex": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$eq": { + "type": "string" + }, + "$ne": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "$in": { + "type": "array", + "items": { + "type": "string" + } + }, + "$nin": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + ] + } + } + }, + "GetCollectionResult": { + "type": "object", + "properties": { + "count": { + "type": "number", + "description": "The Total count of entities" + }, + "entries": { + "type": "array", + "items": { + "$ref": "#/definitions/PublicApiToken" + }, + "description": "List of the selected entities" + } + }, + "required": ["count", "entries"], + "additionalProperties": false, + "description": "Response Model for GetCollection" + }, + "DeleteEndpoint": { + "type": "object", + "properties": { + "url": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + }, + "required": ["id"], + "additionalProperties": false + }, + "result": { + "type": "object" + } + }, + "required": ["url", "result"], + "additionalProperties": false, + "description": "Endpoint model for deleting entities" + } + } +} diff --git a/common/src/apis/github-repositories.ts b/common/src/apis/github-repositories.ts new file mode 100644 index 0000000..975dba5 --- /dev/null +++ b/common/src/apis/github-repositories.ts @@ -0,0 +1,34 @@ +import type { WithOptionalId } from '@furystack/core' +import type { DeleteEndpoint, GetCollectionEndpoint, GetEntityEndpoint, PatchEndpoint, RestApi } from '@furystack/rest' +import type { GitHubRepository } from '../models/github-repository.js' + +export type GitHubRepoWritableFields = Omit + +export type PostGitHubRepoEndpoint = { + result: GitHubRepository + body: WithOptionalId +} + +export type PatchGitHubRepoEndpoint = PatchEndpoint + +export type ValidateRepoEndpoint = { + url: { id: string } + result: { accessible: boolean; message?: string } +} + +export interface GitHubRepositoriesApi extends RestApi { + GET: { + '/github-repositories': GetCollectionEndpoint + '/github-repositories/:id': GetEntityEndpoint + } + POST: { + '/github-repositories': PostGitHubRepoEndpoint + '/github-repositories/:id/validate': ValidateRepoEndpoint + } + PATCH: { + '/github-repositories/:id': PatchGitHubRepoEndpoint + } + DELETE: { + '/github-repositories/:id': DeleteEndpoint + } +} diff --git a/common/src/apis/identity.ts b/common/src/apis/identity.ts new file mode 100644 index 0000000..8e1039a --- /dev/null +++ b/common/src/apis/identity.ts @@ -0,0 +1,27 @@ +import type { RestApi } from '@furystack/rest' +import type { User } from '../models/user.js' + +export type IsAuthenticatedAction = { result: { isAuthenticated: boolean } } +export type GetCurrentUserAction = { result: User } +export type LoginAction = { result: User; body: { username: string; password: string } } +export type LogoutAction = { result: unknown } + +export type PasswordResetAction = { + result: { success: boolean } + body: { + currentPassword: string + newPassword: string + } +} + +export interface IdentityApi extends RestApi { + GET: { + '/isAuthenticated': IsAuthenticatedAction + '/currentUser': GetCurrentUserAction + } + POST: { + '/login': LoginAction + '/logout': LogoutAction + '/password-reset': PasswordResetAction + } +} diff --git a/common/src/apis/index.ts b/common/src/apis/index.ts new file mode 100644 index 0000000..2003e99 --- /dev/null +++ b/common/src/apis/index.ts @@ -0,0 +1,8 @@ +export * from './install.js' +export * from './identity.js' +export * from './stacks.js' +export * from './services.js' +export * from './github-repositories.js' +export * from './prerequisites.js' +export * from './tokens.js' +export * from './system.js' diff --git a/common/src/apis/install.ts b/common/src/apis/install.ts new file mode 100644 index 0000000..7823211 --- /dev/null +++ b/common/src/apis/install.ts @@ -0,0 +1,20 @@ +import type { RestApi } from '@furystack/rest' + +export type InstallState = 'needsInstall' | 'installed' + +export type InstallStateResponse = { + state: InstallState +} + +export type GetServiceStatusAction = { result: InstallStateResponse } + +export type InstallAction = { result: { success: boolean }; body: { username: string; password: string } } + +export interface InstallApi extends RestApi { + GET: { + '/serviceStatus': GetServiceStatusAction + } + POST: { + '/install': InstallAction + } +} diff --git a/common/src/apis/prerequisites.ts b/common/src/apis/prerequisites.ts new file mode 100644 index 0000000..0246c3a --- /dev/null +++ b/common/src/apis/prerequisites.ts @@ -0,0 +1,34 @@ +import type { WithOptionalId } from '@furystack/core' +import type { DeleteEndpoint, GetCollectionEndpoint, GetEntityEndpoint, PatchEndpoint, RestApi } from '@furystack/rest' +import type { Prerequisite } from '../models/prerequisite.js' + +export type PrerequisiteWritableFields = Omit + +export type PostPrerequisiteEndpoint = { + result: Prerequisite + body: WithOptionalId +} + +export type PatchPrerequisiteEndpoint = PatchEndpoint + +export type CheckPrerequisiteEndpoint = { + url: { id: string } + result: { satisfied: boolean; output: string } +} + +export interface PrerequisitesApi extends RestApi { + GET: { + '/prerequisites': GetCollectionEndpoint + '/prerequisites/:id': GetEntityEndpoint + } + POST: { + '/prerequisites': PostPrerequisiteEndpoint + '/prerequisites/:id/check': CheckPrerequisiteEndpoint + } + PATCH: { + '/prerequisites/:id': PatchPrerequisiteEndpoint + } + DELETE: { + '/prerequisites/:id': DeleteEndpoint + } +} diff --git a/common/src/apis/services.ts b/common/src/apis/services.ts new file mode 100644 index 0000000..5648c09 --- /dev/null +++ b/common/src/apis/services.ts @@ -0,0 +1,71 @@ +import type { WithOptionalId } from '@furystack/core' +import type { DeleteEndpoint, GetCollectionEndpoint, GetEntityEndpoint, PatchEndpoint, RestApi } from '@furystack/rest' +import type { ServiceConfig } from '../models/service-config.js' +import type { ServiceDefinition } from '../models/service-definition.js' +import type { ServiceLogEntry } from '../models/service-log-entry.js' +import type { ServiceStateHistory } from '../models/service-state-history.js' +import type { ServiceView } from '../models/views.js' + +export type ServiceDefinitionWritableFields = Omit +export type ServiceConfigWritableFields = Omit +export type ServiceWritableFields = ServiceDefinitionWritableFields & Omit + +export type PostServiceEndpoint = { + result: ServiceView + body: WithOptionalId +} + +export type PatchServiceEndpoint = PatchEndpoint + +export type ServiceActionEndpoint = { url: { id: string }; result: { success: boolean; serviceId: string } } + +export type ApplyServiceFilesEndpoint = { + url: { id: string } + body: { relativePath?: string } + result: { success: boolean; serviceId: string; applied: string[] } +} + +export type ServiceLogsEndpoint = { + url: { id: string } + query: { lines?: number; processUid?: string; search?: string } + result: { entries: ServiceLogEntry[] } +} + +export type ClearServiceLogsEndpoint = { + url: { id: string } + result: { success: boolean } +} + +export type ServiceHistoryEndpoint = { + url: { id: string } + query: { limit?: number } + result: { entries: ServiceStateHistory[] } +} + +export interface ServicesApi extends RestApi { + GET: { + '/services': GetCollectionEndpoint + '/services/:id': GetEntityEndpoint + '/services/:id/logs': ServiceLogsEndpoint + '/services/:id/history': ServiceHistoryEndpoint + } + POST: { + '/services': PostServiceEndpoint + '/services/:id/start': ServiceActionEndpoint + '/services/:id/stop': ServiceActionEndpoint + '/services/:id/restart': ServiceActionEndpoint + '/services/:id/install': ServiceActionEndpoint + '/services/:id/build': ServiceActionEndpoint + '/services/:id/pull': ServiceActionEndpoint + '/services/:id/setup': ServiceActionEndpoint + '/services/:id/update': ServiceActionEndpoint + '/services/:id/apply-files': ApplyServiceFilesEndpoint + } + PATCH: { + '/services/:id': PatchServiceEndpoint + } + DELETE: { + '/services/:id': DeleteEndpoint + '/services/:id/logs': ClearServiceLogsEndpoint + } +} diff --git a/common/src/apis/stacks.ts b/common/src/apis/stacks.ts new file mode 100644 index 0000000..d4a648e --- /dev/null +++ b/common/src/apis/stacks.ts @@ -0,0 +1,74 @@ +import type { WithOptionalId } from '@furystack/core' +import type { DeleteEndpoint, GetCollectionEndpoint, GetEntityEndpoint, PatchEndpoint, RestApi } from '@furystack/rest' +import type { EnvironmentVariableValue } from '../models/environment-variable-value.js' +import type { GitHubRepository } from '../models/github-repository.js' +import type { Prerequisite } from '../models/prerequisite.js' +import type { ServiceConfig } from '../models/service-config.js' +import type { ServiceDefinition } from '../models/service-definition.js' +import type { StackConfig } from '../models/stack-config.js' +import type { StackDefinition } from '../models/stack-definition.js' +import type { StackView } from '../models/views.js' + +export type StackWritableFields = Omit & + Omit +export type PostStackEndpoint = { result: StackView; body: WithOptionalId } +export type PatchStackEndpoint = PatchEndpoint + +type ShareableStackDefinition = Omit +type ShareableServiceDefinition = Omit +type ShareableGitHubRepository = Omit +type ShareablePrerequisite = Omit + +export type ExportStackEndpoint = { + url: { id: string } + result: { + stack: ShareableStackDefinition + services: ShareableServiceDefinition[] + repositories: ShareableGitHubRepository[] + prerequisites: ShareablePrerequisite[] + } +} + +export type ImportStackEndpoint = { + result: { success: boolean } + body: { + stack: ShareableStackDefinition + services: ShareableServiceDefinition[] + repositories: ShareableGitHubRepository[] + prerequisites: ShareablePrerequisite[] + config: { + mainDirectory: string + environmentVariables?: Record + services?: Record< + string, + Partial> & { + environmentVariableOverrides?: Record + } + > + } + } +} + +export type StackSetupEndpoint = { + url: { id: string } + result: { success: boolean } +} + +export interface StacksApi extends RestApi { + GET: { + '/stacks': GetCollectionEndpoint + '/stacks/:id': GetEntityEndpoint + '/stacks/:id/export': ExportStackEndpoint + } + POST: { + '/stacks': PostStackEndpoint + '/stacks/import': ImportStackEndpoint + '/stacks/:id/setup': StackSetupEndpoint + } + PATCH: { + '/stacks/:id': PatchStackEndpoint + } + DELETE: { + '/stacks/:id': DeleteEndpoint + } +} diff --git a/common/src/apis/system.ts b/common/src/apis/system.ts new file mode 100644 index 0000000..9e97ed1 --- /dev/null +++ b/common/src/apis/system.ts @@ -0,0 +1,12 @@ +import type { RestApi } from '@furystack/rest' + +export type CheckEnvAvailabilityEndpoint = { + result: Record + body: { variableNames: string[] } +} + +export interface SystemApi extends RestApi { + POST: { + '/system/check-env-availability': CheckEnvAvailabilityEndpoint + } +} diff --git a/common/src/apis/tokens.ts b/common/src/apis/tokens.ts new file mode 100644 index 0000000..fba8f2e --- /dev/null +++ b/common/src/apis/tokens.ts @@ -0,0 +1,20 @@ +import type { DeleteEndpoint, GetCollectionEndpoint, RestApi } from '@furystack/rest' +import type { ApiToken } from '../models/api-token.js' +import type { PublicApiToken } from '../models/public-api-token.js' + +export type CreateTokenEndpoint = { + result: { token: PublicApiToken; plainTextToken: string } + body: { name: string } +} + +export interface TokensApi extends RestApi { + GET: { + '/tokens': GetCollectionEndpoint + } + POST: { + '/tokens': CreateTokenEndpoint + } + DELETE: { + '/tokens/:id': DeleteEndpoint + } +} diff --git a/common/src/bin/create-schemas.ts b/common/src/bin/create-schemas.ts index 940c7aa..28c2bab 100644 --- a/common/src/bin/create-schemas.ts +++ b/common/src/bin/create-schemas.ts @@ -8,9 +8,6 @@ export interface SchemaGenerationSetting { type: string } -/** - * Entity schemas, e.g. User, Session, etc... - */ export const entityValues: SchemaGenerationSetting[] = [ { inputFile: './src/models/*.ts', @@ -21,8 +18,43 @@ export const entityValues: SchemaGenerationSetting[] = [ export const apiValues: SchemaGenerationSetting[] = [ { - inputFile: './src/stack-craft-api.ts', - outputFile: './schemas/stack-craft-api.json', + inputFile: './src/apis/identity.ts', + outputFile: './schemas/identity-api.json', + type: '*', + }, + { + inputFile: './src/apis/install.ts', + outputFile: './schemas/install-api.json', + type: '*', + }, + { + inputFile: './src/apis/stacks.ts', + outputFile: './schemas/stacks-api.json', + type: '*', + }, + { + inputFile: './src/apis/services.ts', + outputFile: './schemas/services-api.json', + type: '*', + }, + { + inputFile: './src/apis/github-repositories.ts', + outputFile: './schemas/github-repositories-api.json', + type: '*', + }, + { + inputFile: './src/apis/prerequisites.ts', + outputFile: './schemas/prerequisites-api.json', + type: '*', + }, + { + inputFile: './src/apis/tokens.ts', + outputFile: './schemas/tokens-api.json', + type: '*', + }, + { + inputFile: './src/apis/system.ts', + outputFile: './schemas/system-api.json', type: '*', }, ] @@ -35,7 +67,6 @@ export const exec = async (): Promise => { path: join(process.cwd(), schemaValue.inputFile), tsconfig: join(process.cwd(), './tsconfig.json'), skipTypeCheck: true, - // expose: 'all', }).createSchema(schemaValue.type) await promises.writeFile(join(process.cwd(), schemaValue.outputFile), JSON.stringify(schema, null, 2)) } catch (error) { diff --git a/common/src/boilerplate-api.ts b/common/src/boilerplate-api.ts deleted file mode 100644 index 39380ad..0000000 --- a/common/src/boilerplate-api.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { RestApi } from '@furystack/rest' -import type { User } from './models/index.js' - -export type TestQueryEndpoint = { query: { param1: string }; result: { param1Value: string } } -export type TestUrlParamsEndpoint = { url: { urlParam: string }; result: { urlParamValue: string } } -export type TestPostBodyEndpoint = { body: { value: string }; result: { bodyValue: string } } - -export interface StackCraftApi extends RestApi { - GET: { - '/isAuthenticated': { result: { isAuthenticated: boolean } } - '/currentUser': { result: User } - '/testQuery': TestQueryEndpoint - '/testUrlParams/:urlParam': TestUrlParamsEndpoint - } - POST: { - '/login': { result: User; body: { username: string; password: string } } - '/logout': { result: unknown } - '/testPostBody': TestPostBodyEndpoint - } -} diff --git a/common/src/index.ts b/common/src/index.ts index 227ee11..ff71825 100644 --- a/common/src/index.ts +++ b/common/src/index.ts @@ -1,2 +1,4 @@ export * from './models/index.js' -export * from './stack-craft-api.js' +export * from './apis/index.js' +export * from './websocket/index.js' +export * from './utils/service-path-utils.js' diff --git a/common/src/models/api-token.ts b/common/src/models/api-token.ts new file mode 100644 index 0000000..514f67e --- /dev/null +++ b/common/src/models/api-token.ts @@ -0,0 +1,8 @@ +export class ApiToken { + id!: string + username!: string + name!: string + tokenHash!: string + lastUsedAt?: string + createdAt!: string +} diff --git a/common/src/models/environment-variable-value.ts b/common/src/models/environment-variable-value.ts new file mode 100644 index 0000000..d238433 --- /dev/null +++ b/common/src/models/environment-variable-value.ts @@ -0,0 +1,11 @@ +/** + * Describes how an environment variable's value is resolved. + * Used in {@link StackConfig} for stack-level defaults and + * in {@link ServiceConfig} for per-service overrides. + */ +export type EnvironmentVariableValue = { + /** Whether to inherit the value from the host system or use a custom value */ + source: 'inherit' | 'custom' + /** The custom value to use when source is 'custom' */ + customValue?: string +} diff --git a/common/src/models/github-repository.ts b/common/src/models/github-repository.ts new file mode 100644 index 0000000..0e05d16 --- /dev/null +++ b/common/src/models/github-repository.ts @@ -0,0 +1,24 @@ +/** + * Shareable GitHub repository definition. + * Links a git repository to a stack for cloning and pulling. + * Included in stack exports and shared between installations. + */ +export class GitHubRepository { + /** UUID primary key */ + id!: string + + /** FK to {@link StackDefinition.name} */ + stackName!: string + + /** Full URL to the git repository (e.g. "https://github.com/user/repo") */ + url!: string + + /** Human-readable name shown in the UI */ + displayName!: string + + /** Optional description */ + description: string = '' + + createdAt!: string + updatedAt!: string +} diff --git a/common/src/models/index.ts b/common/src/models/index.ts index 72e50f8..0530dc7 100644 --- a/common/src/models/index.ts +++ b/common/src/models/index.ts @@ -1 +1,15 @@ export * from './user.js' +export * from './environment-variable-value.js' +export * from './stack-definition.js' +export * from './stack-config.js' +export * from './service-definition.js' +export * from './service-config.js' +export * from './service-status.js' +export * from './service-state-history.js' +export * from './service-log-entry.js' +export * from './github-repository.js' +export * from './prerequisite.js' +export * from './prerequisite-check-result.js' +export * from './api-token.js' +export * from './public-api-token.js' +export * from './views.js' diff --git a/common/src/models/prerequisite-check-result.ts b/common/src/models/prerequisite-check-result.ts new file mode 100644 index 0000000..80d18d0 --- /dev/null +++ b/common/src/models/prerequisite-check-result.ts @@ -0,0 +1,23 @@ +/** + * Possible statuses for a prerequisite check. + */ +export type PrerequisiteCheckStatus = 'unchecked' | 'checking' | 'satisfied' | 'failed' + +/** + * Represents the result of evaluating a {@link Prerequisite}. + * Stored in an in-memory store on the service and synced to the frontend + * via entity sync so that every connected client sees real-time status. + */ +export class PrerequisiteCheckResult { + /** PK – corresponds to {@link Prerequisite.id} */ + prerequisiteId!: string + + /** Current check status */ + status!: PrerequisiteCheckStatus + + /** Human-readable output from the last check */ + output: string = '' + + /** ISO-8601 timestamp of the last check */ + checkedAt: string = '' +} diff --git a/common/src/models/prerequisite.ts b/common/src/models/prerequisite.ts new file mode 100644 index 0000000..200bbc1 --- /dev/null +++ b/common/src/models/prerequisite.ts @@ -0,0 +1,62 @@ +/** + * The type of prerequisite that must be satisfied before a stack or service can run. + */ +export type PrerequisiteType = + | 'node' + | 'yarn' + | 'dotnet-sdk' + | 'dotnet-runtime' + | 'nuget-feed' + | 'git' + | 'github-cli' + | 'env-variable' + | 'custom-script' + +/** + * Maps each prerequisite type to its type-specific configuration shape. + */ +export type PrerequisiteConfigMap = { + node: { minimumVersion: string } + yarn: { minimumVersion: string } + 'dotnet-sdk': { version: string } + 'dotnet-runtime': { version: string } + 'nuget-feed': { feedUrl: string; feedName?: string } + git: Record + 'github-cli': Record + 'env-variable': { variableName: string } + 'custom-script': { script: string } +} + +/** + * Union of all possible prerequisite config shapes. + */ +export type PrerequisiteConfig = PrerequisiteConfigMap[PrerequisiteType] + +/** + * Shareable prerequisite definition. + * Describes an external requirement (e.g. Node.js, Git, an env variable) + * that a stack or service may need to be satisfied before it can run. + * Included in stack exports and shared between installations. + */ +export class Prerequisite { + /** UUID primary key */ + id!: string + + /** FK to {@link StackDefinition.name} */ + stackName!: string + + /** Human-readable prerequisite name (e.g. "Node.js >= 18") */ + name!: string + + /** Discriminator that determines the check logic and config shape */ + type!: PrerequisiteType + + /** Type-specific configuration (stored as JSON TEXT in the database) */ + config!: PrerequisiteConfig + + /** Help text shown when the prerequisite check fails */ + installationHelp: string = '' + + createdAt!: string + updatedAt!: string +} diff --git a/common/src/models/public-api-token.ts b/common/src/models/public-api-token.ts new file mode 100644 index 0000000..0b65c80 --- /dev/null +++ b/common/src/models/public-api-token.ts @@ -0,0 +1,7 @@ +export class PublicApiToken { + id!: string + username!: string + name!: string + lastUsedAt?: string + createdAt!: string +} diff --git a/common/src/models/service-config.ts b/common/src/models/service-config.ts new file mode 100644 index 0000000..1016239 --- /dev/null +++ b/common/src/models/service-config.ts @@ -0,0 +1,27 @@ +import type { EnvironmentVariableValue } from './environment-variable-value.js' + +/** + * User-specific service configuration. + * Contains settings that each installation can customize independently. + * Not included in exports - set by the user during import/installation. + * @see ServiceDefinition for the shareable definition + */ +export class ServiceConfig { + /** FK to {@link ServiceDefinition.id} */ + serviceId!: string + + /** Whether automatic git fetch is enabled */ + autoFetchEnabled: boolean = false + + /** Interval in minutes between automatic git fetches */ + autoFetchIntervalMinutes: number = 60 + + /** Whether to automatically restart the service when new commits are fetched */ + autoRestartOnFetch: boolean = false + + /** Per-service environment variable overrides, keyed by variable name. Overrides stack-level defaults. */ + environmentVariableOverrides: Record = {} + + createdAt!: string + updatedAt!: string +} diff --git a/common/src/models/service-definition.ts b/common/src/models/service-definition.ts new file mode 100644 index 0000000..bfb6f5e --- /dev/null +++ b/common/src/models/service-definition.ts @@ -0,0 +1,55 @@ +/** A file to be placed relative to the service root (e.g. .env, appConfig.local.json) */ +export type ServiceFile = { + /** Path relative to the service working directory */ + relativePath: string + /** File content (usually plain text) */ + content: string +} + +/** + * Shareable service definition. + * Contains the immutable description of a service and its commands. + * Included in stack exports and shared between installations. + * @see ServiceConfig for user-specific configuration + * @see ServiceStatus for runtime state + */ +export class ServiceDefinition { + /** UUID primary key */ + id!: string + + /** FK to {@link StackDefinition.name} */ + stackName!: string + + /** Human-readable name shown in the UI */ + displayName!: string + + /** Optional description of what this service does */ + description: string = '' + + /** Optional relative path within stack for grouping (e.g. "frontends/public") */ + workingDirectory?: string + + /** Optional FK to {@link GitHubRepository.id} */ + repositoryId?: string + + /** IDs of {@link Prerequisite} entities required by this service */ + prerequisiteIds: string[] = [] + + /** IDs of other {@link ServiceDefinition} entities that must be running first */ + prerequisiteServiceIds: string[] = [] + + /** Shell command to install dependencies (e.g. "npm install") */ + installCommand?: string + + /** Shell command to build the service (e.g. "npm run build") */ + buildCommand?: string + + /** Shell command to run the service (e.g. "npm start") */ + runCommand!: string + + /** Shared files placed relative to the service root */ + files: ServiceFile[] = [] + + createdAt!: string + updatedAt!: string +} diff --git a/common/src/models/service-log-entry.ts b/common/src/models/service-log-entry.ts new file mode 100644 index 0000000..2fe3ddd --- /dev/null +++ b/common/src/models/service-log-entry.ts @@ -0,0 +1,8 @@ +export class ServiceLogEntry { + id!: number + serviceId!: string + processUid!: string + stream!: 'stdout' | 'stderr' + line!: string + createdAt!: string +} diff --git a/common/src/models/service-state-history.ts b/common/src/models/service-state-history.ts new file mode 100644 index 0000000..5461c11 --- /dev/null +++ b/common/src/models/service-state-history.ts @@ -0,0 +1,70 @@ +/** + * Event types for service state transitions. + * Each value represents a discrete lifecycle event that can occur. + */ +export type ServiceStateEvent = + | 'clone-started' + | 'clone-completed' + | 'clone-failed' + | 'run-started' + | 'run-stopped' + | 'run-crashed' + | 'run-restarted' + | 'install-started' + | 'install-completed' + | 'install-failed' + | 'build-started' + | 'build-completed' + | 'build-failed' + | 'setup-started' + | 'setup-completed' + | 'setup-failed' + | 'update-started' + | 'update-completed' + | 'update-failed' + | 'pull-completed' + | 'imported' + | 'state-reconciled' + +/** + * How a state change was triggered. + * Used to distinguish user actions from automated system behavior. + */ +export type TriggerSource = 'api' | 'mcp' | 'auto-fetch' | 'auto-restart' | 'system' + +/** + * Audit log entry for service state transitions. + * Records every lifecycle event (start, stop, crash, install, build, pull) + * with full context: who triggered it, how, and any relevant metadata. + * Entries are never deleted and not included in exports. + */ +export class ServiceStateHistory { + /** Auto-increment primary key */ + id!: number + + /** FK to {@link ServiceDefinition.id} */ + serviceId!: string + + /** The lifecycle event that occurred */ + event!: ServiceStateEvent + + /** JSON snapshot of relevant status fields before the change */ + previousState?: string + + /** JSON snapshot of relevant status fields after the change */ + newState?: string + + /** Username of the user who triggered the action, or 'system' */ + triggeredBy!: string + + /** How the action was triggered */ + triggerSource!: TriggerSource + + /** Optional JSON with extra context (exit code, error message, etc.) */ + metadata?: string + + /** UUID of the associated process, if this event produced log output */ + processUid?: string + + createdAt!: string +} diff --git a/common/src/models/service-status.ts b/common/src/models/service-status.ts new file mode 100644 index 0000000..f7541a1 --- /dev/null +++ b/common/src/models/service-status.ts @@ -0,0 +1,29 @@ +export type CloneStatus = 'not-cloned' | 'cloning' | 'cloned' | 'failed' +export type InstallStatus = 'not-installed' | 'installing' | 'installed' | 'failed' +export type BuildStatus = 'not-built' | 'building' | 'built' | 'failed' +export type RunStatus = 'stopped' | 'starting' | 'running' | 'stopping' | 'error' + +/** + * Runtime status of a service. + * Managed by the system (ProcessManager, GitWatcher). + * Never exported. Reset to defaults on import. + * @see ServiceDefinition for the shareable definition + * @see ServiceStateHistory for the audit log of state transitions + */ +export class ServiceStatus { + /** FK to {@link ServiceDefinition.id} */ + serviceId!: string + + cloneStatus: CloneStatus = 'not-cloned' + installStatus: InstallStatus = 'not-installed' + buildStatus: BuildStatus = 'not-built' + runStatus: RunStatus = 'stopped' + + lastClonedAt?: string + lastInstalledAt?: string + lastBuiltAt?: string + lastStartedAt?: string + lastFetchedAt?: string + + updatedAt!: string +} diff --git a/common/src/models/stack-config.ts b/common/src/models/stack-config.ts new file mode 100644 index 0000000..fa861bb --- /dev/null +++ b/common/src/models/stack-config.ts @@ -0,0 +1,21 @@ +import type { EnvironmentVariableValue } from './environment-variable-value.js' + +/** + * User-specific stack configuration. + * Contains settings unique to this installation/machine. + * Not included in exports - set by the user during import/installation. + * @see StackDefinition for the shareable definition + */ +export class StackConfig { + /** FK to {@link StackDefinition.name} */ + stackName!: string + + /** Absolute path to the root directory for all services in this stack */ + mainDirectory!: string + + /** Stack-level environment variable values, keyed by variable name */ + environmentVariables: Record = {} + + createdAt!: string + updatedAt!: string +} diff --git a/common/src/models/stack-definition.ts b/common/src/models/stack-definition.ts new file mode 100644 index 0000000..71ea4e8 --- /dev/null +++ b/common/src/models/stack-definition.ts @@ -0,0 +1,19 @@ +/** + * Shareable stack definition. + * Contains the immutable identity and description of a stack. + * Included in stack exports and shared between installations. + * @see StackConfig for user-specific configuration + */ +export class StackDefinition { + /** Unique kebab-case identifier for the stack */ + name!: string + + /** Human-readable name shown in the UI */ + displayName!: string + + /** Optional description of what this stack does */ + description: string = '' + + createdAt!: string + updatedAt!: string +} diff --git a/common/src/models/views.ts b/common/src/models/views.ts new file mode 100644 index 0000000..ab6a3b5 --- /dev/null +++ b/common/src/models/views.ts @@ -0,0 +1,11 @@ +import type { StackConfig } from './stack-config.js' +import type { StackDefinition } from './stack-definition.js' +import type { ServiceConfig } from './service-config.js' +import type { ServiceDefinition } from './service-definition.js' +import type { ServiceStatus } from './service-status.js' + +/** Full stack view combining definition and config for API responses */ +export type StackView = StackDefinition & StackConfig + +/** Full service view combining definition, config, and status for API responses */ +export type ServiceView = ServiceDefinition & ServiceConfig & ServiceStatus diff --git a/common/src/stack-craft-api.ts b/common/src/stack-craft-api.ts deleted file mode 100644 index 39380ad..0000000 --- a/common/src/stack-craft-api.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { RestApi } from '@furystack/rest' -import type { User } from './models/index.js' - -export type TestQueryEndpoint = { query: { param1: string }; result: { param1Value: string } } -export type TestUrlParamsEndpoint = { url: { urlParam: string }; result: { urlParamValue: string } } -export type TestPostBodyEndpoint = { body: { value: string }; result: { bodyValue: string } } - -export interface StackCraftApi extends RestApi { - GET: { - '/isAuthenticated': { result: { isAuthenticated: boolean } } - '/currentUser': { result: User } - '/testQuery': TestQueryEndpoint - '/testUrlParams/:urlParam': TestUrlParamsEndpoint - } - POST: { - '/login': { result: User; body: { username: string; password: string } } - '/logout': { result: unknown } - '/testPostBody': TestPostBodyEndpoint - } -} diff --git a/common/src/utils/service-path-utils.spec.ts b/common/src/utils/service-path-utils.spec.ts new file mode 100644 index 0000000..8a3d9e1 --- /dev/null +++ b/common/src/utils/service-path-utils.spec.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest' + +import { getRepoNameFromUrl, getServiceCwd } from './service-path-utils.js' +import type { GitHubRepository } from '../models/index.js' + +describe('service-path-utils', () => { + describe('getRepoNameFromUrl', () => { + it('should extract repo name from .git URL', () => { + expect(getRepoNameFromUrl('https://github.com/user/my-repo.git')).toBe('my-repo') + }) + + it('should extract repo name from URL without .git', () => { + expect(getRepoNameFromUrl('https://github.com/user/my-repo')).toBe('my-repo') + }) + + it('should return "repo" for malformed URL', () => { + expect(getRepoNameFromUrl('invalid')).toBe('repo') + }) + }) + + describe('getServiceCwd', () => { + const config = { mainDirectory: '/workspace/stacks/my-stack' } + + it('should join stack root with service path and repo name when repo is linked', () => { + const service = { workingDirectory: 'frontends/public' } + const repo: GitHubRepository = { + id: 'repo-1', + stackName: 'my-stack', + url: 'https://github.com/org/my-frontend.git', + displayName: 'My Frontend', + description: '', + createdAt: '', + updatedAt: '', + } + expect(getServiceCwd(config, service, repo)).toBe('/workspace/stacks/my-stack/frontends/public/my-frontend') + }) + + it('should use stack root when service has no workingDirectory and no repo', () => { + const service = {} + expect(getServiceCwd(config, service, null)).toBe('/workspace/stacks/my-stack') + }) + + it('should use stack root + service path when no repo', () => { + const service = { workingDirectory: 'services/gateway' } + expect(getServiceCwd(config, service, null)).toBe('/workspace/stacks/my-stack/services/gateway') + }) + }) +}) diff --git a/common/src/utils/service-path-utils.ts b/common/src/utils/service-path-utils.ts new file mode 100644 index 0000000..fcb662b --- /dev/null +++ b/common/src/utils/service-path-utils.ts @@ -0,0 +1,38 @@ +import type { GitHubRepository } from '../models/index.js' + +function joinPath(...parts: Array): string { + return parts + .filter((p): p is string => typeof p === 'string' && p !== '') + .map((p) => p.replace(/\\/g, '/')) + .join('/') + .replace(/\/+/g, '/') +} + +/** + * Extracts the repository name from a Git URL. + * e.g. "https://github.com/user/my-repo.git" -> "my-repo" + */ +export function getRepoNameFromUrl(url: string): string { + const match = url.match(/\/([^/]+?)(?:\.git)?$/i) + return match ? match[1] : 'repo' +} + +/** + * Computes the full working directory (CWD) for a service. + * Formula: join(mainDirectory, workingDirectory ?? '', repoName when cloned) + * + * - mainDirectory comes from {@link StackConfig} and is the root for all processes in the stack + * - workingDirectory comes from {@link ServiceDefinition} and is optional, for grouping + * - When cloning from Git, the repo name is added as a subdirectory + */ +export function getServiceCwd( + config: { mainDirectory: string }, + service: { workingDirectory?: string }, + repo?: GitHubRepository | null, +): string { + const base = joinPath(config.mainDirectory, service.workingDirectory) + if (repo?.url) { + return joinPath(base, getRepoNameFromUrl(repo.url)) + } + return base +} diff --git a/common/src/websocket/index.ts b/common/src/websocket/index.ts new file mode 100644 index 0000000..c87bd5b --- /dev/null +++ b/common/src/websocket/index.ts @@ -0,0 +1,22 @@ +import type { CloneStatus, InstallStatus, BuildStatus, RunStatus } from '../models/service-status.js' + +export type WebsocketMessage = + | { + type: 'service-status-changed' + serviceId: string + cloneStatus: CloneStatus + installStatus: InstallStatus + buildStatus: BuildStatus + runStatus: RunStatus + } + | { + type: 'git-branches-changed' + serviceId: string + newBranches: string[] + } + | { + type: 'dependency-check-result' + dependencyId: string + satisfied: boolean + output: string + } diff --git a/e2e/page.spec.ts b/e2e/page.spec.ts index b1be8f4..6bc89e2 100644 --- a/e2e/page.spec.ts +++ b/e2e/page.spec.ts @@ -1,38 +1,81 @@ import { expect, test } from '@playwright/test' -test.describe('Example Application', () => { - test('Login and logout roundtrip', async ({ page }) => { +test.describe('StackCraft MVP Flow', () => { + test('should complete the installer wizard', async ({ page }) => { + await page.goto('/') + + const installerOrLogin = page.locator('shade-lazy-installer, shade-login') + await expect(installerOrLogin.first()).toBeVisible({ timeout: 15000 }) + + const isInstaller = (await page.locator('shade-lazy-installer').count()) > 0 + if (!isInstaller) { + return + } + + const nextButton = page.locator('button', { hasText: 'Next' }) + + await expect(page.locator('text=Welcome')).toBeVisible({ timeout: 10000 }) + await nextButton.click() + + await expect(page.locator('text=Prerequisites')).toBeVisible({ timeout: 5000 }) + await nextButton.click() + + await expect(page.locator('text=Admin')).toBeVisible({ timeout: 5000 }) + const usernameInput = page.locator('input[name="userName"]') + const passwordInput = page.locator('input[name="password"]') + await usernameInput.fill('testuser') + await passwordInput.fill('password') + await page.locator('button', { hasText: 'Create' }).click() + + await expect(page.locator('text=Success')).toBeVisible({ timeout: 10000 }) + await page.locator('button', { hasText: 'Finish' }).click() + }) + + test('Login and view dashboard', async ({ page }) => { await page.goto('/') const loginForm = page.locator('shade-login form') - await expect(loginForm).toBeVisible() + await expect(loginForm).toBeVisible({ timeout: 15000 }) + + await loginForm.locator('input[name="userName"]').fill('testuser') + await loginForm.locator('input[name="password"]').fill('password') + await page.locator('button', { hasText: 'Login' }).click() + + await expect(page.locator('shade-dashboard')).toBeVisible({ timeout: 10000 }) + }) + + test('Create stack, create service, verify dashboard', async ({ page }) => { + await page.goto('/') - const usernameInput = loginForm.locator('input[name="userName"]') - await expect(usernameInput).toBeVisible() + const loginForm = page.locator('shade-login form') + await expect(loginForm).toBeVisible({ timeout: 15000 }) + await loginForm.locator('input[name="userName"]').fill('testuser') + await loginForm.locator('input[name="password"]').fill('password') + await page.locator('button', { hasText: 'Login' }).click() - const passwordInput = loginForm.locator('input[name="password"]') - await expect(passwordInput).toBeVisible() + await expect(page.locator('shade-dashboard')).toBeVisible({ timeout: 10000 }) - await usernameInput.type('testuser') - await passwordInput.type('password') + await page.locator('button', { hasText: 'Create Stack' }).first().click() + await expect(page.locator('shade-create-stack')).toBeVisible({ timeout: 5000 }) - const submitButton = page.locator('button', { hasText: 'Login' }) - await expect(submitButton).toBeVisible() - await expect(submitButton).toBeEnabled() + await page.locator('input[name="name"]').fill('e2e-test-stack') + await page.locator('input[name="displayName"]').fill('E2E Test Stack') + await page.locator('input[name="description"]').fill('Created by E2E test') + await page.locator('input[name="mainDirectory"]').fill('/tmp/e2e-test') + await page.locator('button', { hasText: 'Create' }).click() - await submitButton.click() + await expect(page.locator('shade-dashboard')).toBeVisible({ timeout: 10000 }) + await expect(page.locator('text=E2E Test Stack')).toBeVisible({ timeout: 5000 }) - const welcomeTitle = page.locator('hello-world div h2') - await expect(welcomeTitle).toBeVisible() - await expect(welcomeTitle).toHaveText('Hello, testuser !') + await page.locator('button', { hasText: 'Create Service' }).first().click() + await expect(page.locator('shade-create-service')).toBeVisible({ timeout: 5000 }) - const logoutButton = page.locator('shade-app-bar button >> text="Log Out"') - await expect(logoutButton).toBeVisible() - await expect(logoutButton).toBeEnabled() - await expect(logoutButton).toHaveText('Log Out') - await logoutButton.click() + await page.locator('input[name="displayName"]').fill('E2E Service') + await page.locator('input[name="workingDirectory"]').fill('svc') + await page.locator('input[name="runCommand"]').fill('echo hello') + await page.locator('button[type="submit"]').click() - const loggedOutLoginForm = page.locator('shade-login form') - await expect(loggedOutLoginForm).toBeVisible() + await expect(page.locator('shade-dashboard')).toBeVisible({ timeout: 10000 }) + await expect(page.locator('text=E2E Service')).toBeVisible({ timeout: 5000 }) }) }) diff --git a/frontend/package.json b/frontend/package.json index 74d4989..b190bd4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,19 +12,22 @@ "license": "ISC", "devDependencies": { "@codecov/vite-plugin": "^1.9.1", + "@types/node": "^25.3.0", "typescript": "^5.9.3", "vite": "^7.3.1", "vitest": "^4.0.18" }, "dependencies": { - "@furystack/core": "^15.0.36", + "@furystack/cache": "^6.0.0", + "@furystack/core": "^15.2.0", + "@furystack/entity-sync": "^1.0.1", + "@furystack/entity-sync-client": "^1.0.1", "@furystack/inject": "^12.0.30", "@furystack/logging": "^8.0.30", - "@furystack/rest-client-fetch": "^8.0.36", - "@furystack/shades": "^12.0.1", - "@furystack/shades-common-components": "^12.1.0", + "@furystack/rest-client-fetch": "^8.0.38", + "@furystack/shades": "^12.2.2", + "@furystack/shades-common-components": "^12.6.0", "@furystack/utils": "^8.1.10", - "@types/node": "^25.2.3", "common": "workspace:^" } } diff --git a/frontend/src/components/body.tsx b/frontend/src/components/body.tsx index 74cefe8..38daec5 100644 --- a/frontend/src/components/body.tsx +++ b/frontend/src/components/body.tsx @@ -1,8 +1,70 @@ -import { createComponent, Router, Shade } from '@furystack/shades' -import { ButtonsDemo, HelloWorld, Init, Login, Offline } from '../pages/index.js' +import type { Injector } from '@furystack/inject' +import { createComponent, NestedRouter, Shade } from '@furystack/shades' +import { Dashboard } from '../pages/dashboard/index.js' +import { ExportStack } from '../pages/import-export/export-stack.js' +import { ImportStack } from '../pages/import-export/import-stack.js' +import { Init, Offline } from '../pages/index.js' +import { CreateRepository } from '../pages/repositories/create-repository.js' +import { EditRepository } from '../pages/repositories/edit-repository.js' +import { CreateService } from '../pages/services/create-service.js' +import { ServiceDetail } from '../pages/services/service-detail.js' +import { ServiceLogs } from '../pages/services/service-logs.js' +import { UserSettings } from '../pages/settings/user-settings.js' +import { CreateStack } from '../pages/stacks/create-stack.js' +import { EditStack } from '../pages/stacks/edit-stack.js' +import { StackSetup } from '../pages/stacks/stack-setup.js' +import { CreateServiceWizard } from '../pages/wizards/create-service-wizard.js' import { SessionService } from '../services/session.js' -export const Body = Shade<{ style?: Partial }>({ +const appRoutes = { + '/': { + component: () => , + }, + '/services/create/:stackName': { + component: ({ match }) => , + }, + '/services/wizard/:stackName': { + component: ({ match }) => , + }, + '/services/:id/logs/:processUid': { + component: ({ match }) => , + }, + '/services/:id/logs': { + component: ({ match }) => , + }, + '/services/:id': { + component: ({ match }) => , + }, + '/repositories/create/:stackName': { + component: ({ match }) => , + }, + '/repositories/:id': { + component: ({ match }) => , + }, + '/settings': { + component: () => , + }, + '/stacks/create': { + component: () => , + }, + '/stacks/import': { + component: () => , + }, + '/stacks/:name/setup': { + component: ({ match }) => , + }, + '/stacks/:name/edit': { + component: ({ match }) => , + }, + '/stacks/:name': { + component: ({ match }) => , + }, + '/stacks/:name/export': { + component: ({ match }) => , + }, +} satisfies Record } }) => JSX.Element }> + +export const Body = Shade<{ style?: Partial; injector?: Injector }>({ shadowDomName: 'shade-app-body', render: ({ injector, useObservable }) => { const session = injector.getInstance(SessionService) @@ -12,18 +74,9 @@ export const Body = Shade<{ style?: Partial }>({ {(() => { switch (sessionState) { case 'authenticated': - return ( - }, - { url: '/', routingOptions: { end: false }, component: () => }, - ]} - > - ) + return } /> case 'offline': return - case 'unauthenticated': - return default: return } diff --git a/frontend/src/components/entity-forms/github-repo-form.tsx b/frontend/src/components/entity-forms/github-repo-form.tsx new file mode 100644 index 0000000..2128961 --- /dev/null +++ b/frontend/src/components/entity-forms/github-repo-form.tsx @@ -0,0 +1,82 @@ +import { createComponent, NestedRouteLink, Shade } from '@furystack/shades' +import { Button, Form, Icon, icons, Input, MarkdownInput } from '@furystack/shades-common-components' +import type { GitHubRepository } from 'common' + +type GitHubRepoFormPayload = { + url: string + displayName: string + description: string +} + +const isGitHubRepoFormPayload = (data: unknown): data is GitHubRepoFormPayload => { + const d = data as GitHubRepoFormPayload + return d.url?.length > 0 && d.displayName?.length > 0 +} + +type GitHubRepoFormProps = { + initial?: Partial + stackName: string + onSubmit: (data: Partial) => void | Promise + onCancel?: () => void + cancelHref?: string + mode: 'create' | 'edit' +} + +export const GitHubRepoForm = Shade({ + shadowDomName: 'shade-github-repo-form', + render: ({ props }) => { + return ( + + validate={isGitHubRepoFormPayload} + onSubmit={(data) => + props.onSubmit({ + stackName: props.stackName, + url: data.url, + displayName: data.displayName, + description: data.description, + }) + } + disableOnSubmit + style={{ display: 'flex', flexDirection: 'column', gap: '16px', maxWidth: '600px' }} + > +

{props.mode === 'create' ? 'Add GitHub Repository' : 'Edit Repository'}

+ 'Full GitHub URL, e.g. https://github.com/org/repo'} + /> + + +
+ {props.cancelHref ? ( + + + + ) : ( + + )} + +
+ + ) + }, +}) diff --git a/frontend/src/components/entity-forms/prerequisite-form.tsx b/frontend/src/components/entity-forms/prerequisite-form.tsx new file mode 100644 index 0000000..18ca788 --- /dev/null +++ b/frontend/src/components/entity-forms/prerequisite-form.tsx @@ -0,0 +1,211 @@ +import { createComponent, Shade } from '@furystack/shades' +import { Button, Form, Icon, icons, Input, MarkdownInput, Select } from '@furystack/shades-common-components' +import type { Prerequisite, PrerequisiteConfig, PrerequisiteType } from 'common' + +type PrerequisiteFormPayload = { + name: string + type: PrerequisiteType + minimumVersion?: string + version?: string + feedUrl?: string + feedName?: string + variableName?: string + script?: string + installationHelp?: string +} + +const TYPES_REQUIRING_MINIMUM_VERSION: PrerequisiteType[] = ['node', 'yarn'] +const TYPES_REQUIRING_VERSION: PrerequisiteType[] = ['dotnet-sdk', 'dotnet-runtime'] + +const isPrerequisiteFormPayload = (data: unknown): data is PrerequisiteFormPayload => { + const d = data as PrerequisiteFormPayload + if (!d.name || d.name.length === 0) return false + if (!d.type || d.type.length === 0) return false + + if (TYPES_REQUIRING_MINIMUM_VERSION.includes(d.type) && (!d.minimumVersion || d.minimumVersion.length === 0)) { + return false + } + if (TYPES_REQUIRING_VERSION.includes(d.type) && (!d.version || d.version.length === 0)) { + return false + } + if (d.type === 'nuget-feed' && (!d.feedUrl || d.feedUrl.length === 0)) return false + if (d.type === 'env-variable' && (!d.variableName || d.variableName.length === 0)) return false + if (d.type === 'custom-script' && (!d.script || d.script.length === 0)) return false + + return true +} + +const buildConfig = (data: PrerequisiteFormPayload): PrerequisiteConfig => { + switch (data.type) { + case 'node': + case 'yarn': + return { minimumVersion: data.minimumVersion! } as PrerequisiteConfig + case 'dotnet-sdk': + case 'dotnet-runtime': + return { version: data.version! } as PrerequisiteConfig + case 'nuget-feed': + return { feedUrl: data.feedUrl!, ...(data.feedName ? { feedName: data.feedName } : {}) } as PrerequisiteConfig + case 'git': + case 'github-cli': + return {} as PrerequisiteConfig + case 'env-variable': + return { variableName: data.variableName! } as PrerequisiteConfig + case 'custom-script': + return { script: data.script! } as PrerequisiteConfig + default: + return {} as PrerequisiteConfig + } +} + +const extractConfigField = (config: PrerequisiteConfig | undefined, field: string): string => { + if (!config || typeof config !== 'object') return '' + return (config as Record)[field] ?? '' +} + +const typeOptions = [ + { value: 'node', label: 'Node.js' }, + { value: 'yarn', label: 'Yarn' }, + { value: 'dotnet-sdk', label: '.NET SDK' }, + { value: 'dotnet-runtime', label: '.NET Runtime' }, + { value: 'nuget-feed', label: 'NuGet Feed' }, + { value: 'git', label: 'Git' }, + { value: 'github-cli', label: 'GitHub CLI (gh)' }, + { value: 'env-variable', label: 'Environment Variable' }, + { value: 'custom-script', label: 'Custom Script' }, +] + +type PrerequisiteFormProps = { + initial?: Partial + stackName: string + onSubmit: (data: Partial) => void | Promise + onCancel: () => void + mode: 'create' | 'edit' +} + +export const PrerequisiteForm = Shade({ + shadowDomName: 'shade-prerequisite-form', + render: ({ props, useState }) => { + const [selectedType, setSelectedType] = useState('selectedType', props.initial?.type ?? '') + + return ( + + validate={isPrerequisiteFormPayload} + onSubmit={(data) => + props.onSubmit({ + stackName: props.stackName, + name: data.name, + type: data.type, + config: buildConfig(data), + installationHelp: data.installationHelp ?? '', + }) + } + disableOnSubmit + style={{ display: 'flex', flexDirection: 'column', gap: '16px', maxWidth: '600px' }} + > +

{props.mode === 'create' ? 'Add Prerequisite' : 'Edit Prerequisite'}

+ 'Human-readable name (e.g., "Node.js >= 18")'} + /> + 'Semver version (e.g., "18.0.0", "4.0.0")'} + /> + )} + + {(selectedType === 'dotnet-sdk' || selectedType === 'dotnet-runtime') && ( + 'Exact version prefix (e.g., "8.0", "9.0.100")'} + /> + )} + + {selectedType === 'nuget-feed' && ( +
+ 'The NuGet feed URL to check for'} + /> + 'Optional display name for the feed'} + /> +
+ )} + + {selectedType === 'env-variable' && ( + 'The environment variable name to check (e.g., "GITHUB_TOKEN")'} + /> + )} + + {selectedType === 'custom-script' && ( + 'Shell script that returns exit code 0 if satisfied'} + /> + )} + + 'Instructions shown when the check fails'} + /> +
+ + +
+ + ) + }, +}) diff --git a/frontend/src/components/entity-forms/service-form.tsx b/frontend/src/components/entity-forms/service-form.tsx new file mode 100644 index 0000000..3f0e44b --- /dev/null +++ b/frontend/src/components/entity-forms/service-form.tsx @@ -0,0 +1,490 @@ +import { createComponent, NestedRouteLink, Shade } from '@furystack/shades' +import { + Button, + Checkbox, + cssVariableTheme, + Form, + Icon, + icons, + Input, + MarkdownInput, + Paper, + Select, +} from '@furystack/shades-common-components' +import type { GitHubRepository, Prerequisite, ServiceFile, ServiceView } from 'common' + +import { prerequisiteTypeLabels } from '../status-chips.js' +import { GitHubRepoForm } from './github-repo-form.js' +import { PrerequisiteForm } from './prerequisite-form.js' + +type ServiceFormPayload = { + displayName: string + description: string + workingDirectory: string + repositoryId: string + runCommand: string + installCommand: string + buildCommand: string + autoFetchEnabled: string + autoFetchIntervalMinutes: string + autoRestartOnFetch: string +} + +const isServiceFormPayload = (data: unknown): data is ServiceFormPayload => { + const d = data as ServiceFormPayload + return d.displayName?.length > 0 && d.runCommand?.length > 0 +} + +type ServiceFormProps = { + initial?: Partial + stackName: string + repositories?: GitHubRepository[] + prerequisites?: Prerequisite[] + otherServices?: Array<{ id: string; displayName: string }> + onSubmit: (data: Partial) => void | Promise + onCreatePrerequisite?: (data: Partial) => Promise + onCreateRepository?: (data: Partial) => Promise + onCancel?: () => void + cancelHref?: string + mode: 'create' | 'edit' +} + +export const ServiceForm = Shade({ + shadowDomName: 'shade-service-form', + render: ({ props, useState }) => { + const [selectedPrereqIds, setSelectedPrereqIds] = useState( + 'selectedPrereqIds', + props.initial?.prerequisiteIds ?? [], + ) + const [selectedPrereqServiceIds, setSelectedPrereqServiceIds] = useState( + 'selectedPrereqServiceIds', + props.initial?.prerequisiteServiceIds ?? [], + ) + const [isCreatingPrereq, setIsCreatingPrereq] = useState('isCreatingPrereq', false) + const [isCreatingRepo, setIsCreatingRepo] = useState('isCreatingRepo', false) + const [sharedFiles, setSharedFiles] = useState('sharedFiles', props.initial?.files ?? []) + + const togglePrereqId = (id: string) => { + const updated = selectedPrereqIds.includes(id) + ? selectedPrereqIds.filter((pid) => pid !== id) + : [...selectedPrereqIds, id] + setSelectedPrereqIds(updated) + } + + const togglePrereqServiceId = (id: string) => { + const updated = selectedPrereqServiceIds.includes(id) + ? selectedPrereqServiceIds.filter((sid) => sid !== id) + : [...selectedPrereqServiceIds, id] + setSelectedPrereqServiceIds(updated) + } + + const repoOptions = [ + { value: '', label: '(None)' }, + ...(props.repositories ?? []).map((r) => ({ value: r.id, label: r.displayName })), + ] + + return ( +
+ + validate={isServiceFormPayload} + onSubmit={(data) => + props.onSubmit({ + stackName: props.stackName, + displayName: data.displayName, + description: data.description, + workingDirectory: data.workingDirectory || undefined, + repositoryId: data.repositoryId || undefined, + runCommand: data.runCommand, + installCommand: data.installCommand || undefined, + buildCommand: data.buildCommand || undefined, + autoFetchEnabled: data.autoFetchEnabled === 'on', + autoFetchIntervalMinutes: parseInt(data.autoFetchIntervalMinutes, 10) || 60, + autoRestartOnFetch: data.autoRestartOnFetch === 'on', + prerequisiteIds: selectedPrereqIds, + prerequisiteServiceIds: selectedPrereqServiceIds, + files: sharedFiles.filter((f) => f.relativePath.trim().length > 0), + }) + } + disableOnSubmit + style={{ display: 'flex', flexDirection: 'column', gap: '16px', maxWidth: '600px' }} + > +

{props.mode === 'create' ? 'Create Service' : 'Edit Service'}

+ +

Definition

+ + + +
+
+ + 'Optional. Relative path within stack for grouping, e.g. frontends/public or services/gateways' + } + /> + 'e.g., npm start, yarn dev, dotnet run'} + /> + 'e.g., npm install, yarn, dotnet restore'} + /> + 'e.g., npm run build, yarn build, dotnet build'} + /> + +
+

Shared Files

+

+ Files placed relative to the service root (e.g. .env, appConfig.local.json). +

+ {sharedFiles.length > 0 ? ( +
+ {sharedFiles.map((file, index) => ( +
+
+ { + const updated = [...sharedFiles] + updated[index] = { ...updated[index], relativePath: (ev.target as HTMLInputElement).value } + setSharedFiles(updated) + }} + style={{ + flex: '1', + padding: '6px 10px', + borderRadius: '4px', + border: '1px solid rgba(255,255,255,0.2)', + background: 'transparent', + color: 'inherit', + fontFamily: 'monospace', + fontSize: '13px', + }} + /> + +
+