48
contrib/claude/agent.md
Normal file
48
contrib/claude/agent.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Agent (`pkg/agent`)
|
||||
|
||||
LLM agent orchestration framework.
|
||||
|
||||
## Agent construction
|
||||
|
||||
```go
|
||||
agent := agent.NewAgent(
|
||||
"agent-name",
|
||||
"System instructions here",
|
||||
agent.WithTools(tool1, tool2),
|
||||
agent.WithHandoffs(otherAgent),
|
||||
agent.WithModel(model),
|
||||
)
|
||||
```
|
||||
|
||||
Functional options: `WithTools`, `WithHandoffs`, `WithInstructions`, `WithModel`, `WithModelSettings`, `WithMCPServers`, `WithInputGuardrails`, `WithOutputGuardrails`, `WithApproval`, `WithSession`.
|
||||
|
||||
## Execution
|
||||
|
||||
```go
|
||||
result, err := agent.Run(ctx, messages)
|
||||
result.FinalMessage().Text() // final output
|
||||
result.LastAgent // agent that produced the result
|
||||
```
|
||||
|
||||
Typed output via `RunTyped[T](ctx, agent, messages)` — validates against JSON Schema.
|
||||
|
||||
## Tool interface
|
||||
|
||||
```go
|
||||
type Tool interface {
|
||||
Name() string
|
||||
Description() string
|
||||
Parameters() jsonschema.Schema
|
||||
Execute(ctx context.Context, input json.RawMessage) (string, error)
|
||||
}
|
||||
```
|
||||
|
||||
## Agent-as-tool
|
||||
|
||||
`agent.AsTool(name, description)` wraps an agent as a tool for composition.
|
||||
|
||||
## Limits
|
||||
|
||||
- Max turns: 10 (default)
|
||||
- Max tool depth: 16 (default)
|
||||
- Depth tracking prevents infinite recursion in handoffs
|
||||
12
contrib/claude/api-surface.md
Normal file
12
contrib/claude/api-surface.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# API Surface Rules
|
||||
|
||||
Every feature must be exposed through **all four interfaces**: GraphQL, MCP, CLI, and n8n. When adding a new endpoint or editing an existing type, keep all four in sync:
|
||||
|
||||
- **GraphQL** — `pkg/server/api/console/v1/graphql/*.graphql` (+ codegen) — see [`contrib/claude/graphql.md`](graphql.md)
|
||||
- **MCP** — `pkg/server/api/mcp/v1/` (+ codegen) — see [`contrib/claude/mcp.md`](mcp.md)
|
||||
- **CLI** — `pkg/cmd/` — see [`contrib/claude/cli.md`](cli.md)
|
||||
- **n8n** — `packages/n8n-node/` — see [`contrib/claude/n8n.md`](n8n.md)
|
||||
|
||||
If you add a mutation in GraphQL, add the corresponding MCP tool, CLI command, and n8n node. If you rename or change a type, update it everywhere.
|
||||
|
||||
Every new Go API endpoint must have end-to-end tests in `e2e/`.
|
||||
@@ -5,6 +5,8 @@ CLI commands use [cobra](https://github.com/spf13/cobra) with `pkg/cmd/cmdutil.F
|
||||
## Directory structure
|
||||
|
||||
```
|
||||
cmd/prb/main.go # Binary entry point
|
||||
pkg/cmd/root/ # Root command, registers all subcommands
|
||||
pkg/cmd/<resource>/<resource>.go # Group command, wires verbs
|
||||
pkg/cmd/<resource>/list/list.go # List verb
|
||||
pkg/cmd/<resource>/create/create.go # Create verb
|
||||
@@ -14,6 +16,7 @@ pkg/cmd/<resource>/delete/delete.go # Delete verb
|
||||
pkg/cmd/cmdutil/ # Factory, flags, output helpers
|
||||
pkg/cmd/iostreams/ # Terminal I/O abstraction
|
||||
pkg/cli/api/ # GraphQL client, pagination
|
||||
pkg/cli/config/ # Config file management (hosts, tokens, default org)
|
||||
```
|
||||
|
||||
Register group commands in `pkg/cmd/root/root.go` with `cmd.AddCommand()`.
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
All raw SQL lives in `pkg/coredata` — never in service, handler, or resolver packages. One file per entity, with companion `*_filter.go` and `*_order_field.go` files when needed.
|
||||
|
||||
- Database: `go.gearno.de/kit/pg`
|
||||
- UUID: `go.gearno.de/crypto/uuid` (never use `github.com/google/uuid`)
|
||||
|
||||
## Entity struct pattern
|
||||
|
||||
Every entity uses `gid.GID` for its ID, `db` tags for pgx mapping, and `CreatedAt`/`UpdatedAt` timestamps. The `tenant_id` column exists in the database but is **never** stored on the Go struct — it is injected at query time via `Scoper`.
|
||||
|
||||
84
contrib/claude/gid.md
Normal file
84
contrib/claude/gid.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# GID — Global Identifiers (`pkg/gid`)
|
||||
|
||||
Every entity ID in the system is a 24-byte tenant-scoped GID, serialized as base64url in the database, JSON, and API surfaces.
|
||||
|
||||
## GID layout (24 bytes / 192 bits)
|
||||
|
||||
| Bytes | Size | Content |
|
||||
|-------|------|---------|
|
||||
| 0–7 | 8 bytes | Tenant ID |
|
||||
| 8–9 | 2 bytes | Entity type (`uint16`) |
|
||||
| 10–17 | 8 bytes | Timestamp (milliseconds since epoch) |
|
||||
| 18–23 | 6 bytes | Random data |
|
||||
|
||||
## Creating a GID
|
||||
|
||||
GIDs are created in the **service layer** (e.g. `pkg/probo/*_service.go`), not in coredata `Insert` methods. The entity type constant comes from `pkg/coredata/entity_type_reg.go`:
|
||||
|
||||
```go
|
||||
assetID := gid.New(s.svc.scope.GetTenantID(), coredata.AssetEntityType)
|
||||
|
||||
asset := &coredata.Asset{
|
||||
ID: assetID,
|
||||
OrganizationID: req.OrganizationID,
|
||||
Name: req.Name,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := asset.Insert(ctx, conn, s.svc.scope)
|
||||
```
|
||||
|
||||
`gid.New` panics on random source failure (should never happen). Use `gid.NewGID` if you need the error.
|
||||
|
||||
## Extracting fields
|
||||
|
||||
```go
|
||||
id.TenantID() // TenantID (first 8 bytes)
|
||||
id.EntityType() // uint16 (bytes 8–9)
|
||||
id.Timestamp() // time.Time (bytes 10–17)
|
||||
```
|
||||
|
||||
## Parsing and serialization
|
||||
|
||||
- `gid.ParseGID(encoded)` — base64url string to GID
|
||||
- `gid.String()` — GID to base64url string
|
||||
- Implements `sql.Scanner`, `driver.Valuer`, `MarshalText`, `UnmarshalText`
|
||||
- `gid.Nil` — zero-value GID
|
||||
|
||||
## TenantID
|
||||
|
||||
`TenantID` is an 8-byte type with its own layout:
|
||||
|
||||
| Bytes | Size | Content |
|
||||
|-------|------|---------|
|
||||
| 0–2 | 3 bytes | Machine ID (random per process) |
|
||||
| 3–5 | 3 bytes | Timestamp (truncated Unix seconds) |
|
||||
| 6–7 | 2 bytes | Atomic counter |
|
||||
|
||||
Create with `gid.NewTenantID()`. Check with `tenantID.IsValid()` (non-nil). Same serialization interfaces as GID (base64url, SQL scanner/valuer).
|
||||
|
||||
## Entity type registry
|
||||
|
||||
All entity type constants live in `pkg/coredata/entity_type_reg.go` as sequential `uint16` values:
|
||||
|
||||
```go
|
||||
const (
|
||||
OrganizationEntityType uint16 = 0
|
||||
FrameworkEntityType uint16 = 1
|
||||
MeasureEntityType uint16 = 2
|
||||
// ...
|
||||
_ uint16 = 8 // PeopleEntityType - removed
|
||||
// ...
|
||||
)
|
||||
```
|
||||
|
||||
**Never reuse removed type numbers.** Use `_` placeholders with a comment noting what was removed. New types get the next available number.
|
||||
|
||||
`NewEntityFromID(id gid.GID) (any, bool)` switches on `id.EntityType()` and returns a pointer to the concrete coredata struct with `ID: id` set, or `nil, false` for unknown types. Add a case here when registering a new entity type.
|
||||
|
||||
## New entity checklist (GID-related steps)
|
||||
|
||||
1. Add `FooEntityType uint16 = N` in the `const` block in `entity_type_reg.go` (next sequential number)
|
||||
2. Add a `case FooEntityType` in `NewEntityFromID` returning `&Foo{ID: id}, true`
|
||||
3. In the service `Create` method, call `gid.New(scope.GetTenantID(), coredata.FooEntityType)` to generate the ID before `Insert`
|
||||
@@ -1,6 +1,40 @@
|
||||
# Go Style
|
||||
|
||||
Layout and readability rules for Go source. (Error handling, naming, and imports are covered in `AGENTS.md` / other guides.)
|
||||
## Project and dependencies
|
||||
|
||||
- HTTP server: `go.gearno.de/kit/httpserver`
|
||||
- HTTP client: `go.gearno.de/kit/httpclient`
|
||||
- Tracing: OpenTelemetry (`go.opentelemetry.io/otel`)
|
||||
- Pointers: Go 1.26 — use `new(expr)` to create pointers to values (e.g. `new(1)`, `new("foo")`, `new(time.Now())`). Use `go.gearno.de/x/ref` only for dereference helpers (`ref.UnrefOrZero`, etc.)
|
||||
|
||||
## Grouped declarations
|
||||
|
||||
Use `type ()`, `const ()`, and `var ()` blocks to group related declarations. Use explicit typed values for string enums, not `iota`.
|
||||
|
||||
```go
|
||||
type (
|
||||
CreateFooRequest struct {
|
||||
Name string
|
||||
Active bool
|
||||
}
|
||||
|
||||
UpdateFooRequest struct {
|
||||
ID gid.GID
|
||||
Name *string
|
||||
Active *bool
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
NameMaxLength = 100
|
||||
ContentMaxLength = 5000
|
||||
)
|
||||
|
||||
var (
|
||||
_ Reader = (*FileReader)(nil)
|
||||
_ Writer = (*FileWriter)(nil)
|
||||
)
|
||||
```
|
||||
|
||||
## Call expressions and argument lists
|
||||
|
||||
@@ -44,3 +78,113 @@ svc, err := foo.NewService(ctx, db, logger, foo.Config{
|
||||
```
|
||||
|
||||
The same rule applies to **method calls** `x.M(a1, …)` — the receiver is already bound; the rule applies to the **argument list** after the method name.
|
||||
|
||||
## Import ordering
|
||||
|
||||
Two groups separated by a blank line: stdlib, then everything else (third-party and internal sorted together alphabetically).
|
||||
|
||||
```go
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
"go.probo.inc/probo/pkg/trust"
|
||||
)
|
||||
```
|
||||
|
||||
## Receiver names
|
||||
|
||||
Short receivers: usually single-letter matching the type (`s` for Service, `c` for Client, `p` for Provider).
|
||||
|
||||
## Error handling
|
||||
|
||||
Wrap errors with `fmt.Errorf` using lowercase messages starting with `cannot`:
|
||||
|
||||
```go
|
||||
return nil, fmt.Errorf("cannot load trust center: %w", err)
|
||||
return nil, fmt.Errorf("cannot create SAML service: %w", err)
|
||||
```
|
||||
|
||||
Sentinel errors in grouped `var ()` blocks. Custom error types implement `Unwrap() error`. Use `errors.Is` for sentinel checks. Use `errors.AsType[T](err)` (generic form) instead of `errors.As(err, &ptr)` for type assertions:
|
||||
|
||||
```go
|
||||
// Good
|
||||
if e, ok := errors.AsType[*ValidationError](err); ok {
|
||||
// use e
|
||||
}
|
||||
|
||||
// Bad — avoid the two-argument form
|
||||
var ve *ValidationError
|
||||
if errors.As(err, &ve) {
|
||||
// use ve
|
||||
}
|
||||
```
|
||||
|
||||
## Naming
|
||||
|
||||
- Constructors: `New*` (e.g. `NewService`, `NewServer`, `NewBridge`)
|
||||
- Config structs: `*Config` suffix (e.g. `APIConfig`, `PgConfig`, `TrustCenterConfig`)
|
||||
- Request structs: `*Request` suffix (e.g. `UpdateTrustCenterRequest`)
|
||||
- Unexported types for internal data: lowercase (e.g. `vendorInfo`, `ctxKey`)
|
||||
|
||||
## Functional options and Config structs
|
||||
|
||||
Use `Config` structs when a constructor has many required parameters. Use functional options (`With*` functions) for optional configuration.
|
||||
|
||||
```go
|
||||
type Option func(*Bridge)
|
||||
|
||||
func WithDryRun(dryRun bool) Option {
|
||||
return func(s *Bridge) {
|
||||
s.dryRun = dryRun
|
||||
}
|
||||
}
|
||||
|
||||
func NewBridge(provider provider.Provider, client *scimclient.Client, opts ...Option) *Bridge {
|
||||
s := &Bridge{provider: provider, scimClient: client}
|
||||
for _, opt := range opts {
|
||||
opt(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
```
|
||||
|
||||
## Interfaces
|
||||
|
||||
Define interfaces in the consumer package. Keep them small. Verify satisfaction at compile time:
|
||||
|
||||
```go
|
||||
var (
|
||||
_ unit.Configurable = (*Implm)(nil)
|
||||
_ unit.Runnable = (*Implm)(nil)
|
||||
)
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
Always first parameter. Private struct keys for context values:
|
||||
|
||||
```go
|
||||
type ctxKey struct{ name string }
|
||||
var trustCenterIDKey = &ctxKey{name: "trust_center_id"}
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
`go.gearno.de/kit/log` — named, context-aware structured logging with typed fields. **Never log PII, PHI, or other sensitive data** (e.g. emails, names, passwords, tokens, health records). Log opaque identifiers (IDs, request IDs) instead.
|
||||
|
||||
```go
|
||||
l.InfoCtx(
|
||||
ctx,
|
||||
"HTTP request to trust center custom domain, redirecting to HTTPS",
|
||||
log.String("domain", domain),
|
||||
log.String("path", r.URL.Path),
|
||||
log.String("to", httpsURL),
|
||||
)
|
||||
```
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Go Testing
|
||||
|
||||
Test library: `github.com/stretchr/testify` (`require` for fatal, `assert` for non-fatal).
|
||||
|
||||
## Package naming
|
||||
|
||||
Black-box test packages (`package foo_test`). White-box (`package foo`) only when testing unexported functions.
|
||||
|
||||
81
contrib/claude/make.md
Normal file
81
contrib/claude/make.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# GNUmakefile
|
||||
|
||||
The project uses a `GNUmakefile` at the root. Builds run with `--jobs=$(nproc)` by default.
|
||||
|
||||
## Everyday targets
|
||||
|
||||
| Target | Purpose |
|
||||
|---|---|
|
||||
| `make build` | Build `bin/probod`, `bin/prb`, and `bin/probod-bootstrap` (includes frontend apps, codegen, and Relay) |
|
||||
| `SKIP_APPS=1 make build` | Build without frontend apps (faster for backend-only work) |
|
||||
| `make test` | Run tests with race detection and coverage |
|
||||
| `make test MODULE=./pkg/foo` | Run tests for a single module |
|
||||
| `make test-verbose` | Tests with verbose output |
|
||||
| `make test-short` | Short tests only |
|
||||
| `make test-bench` | Run benchmarks |
|
||||
| `make test-e2e` | Run console end-to-end tests (requires `bin/probod`) |
|
||||
| `make lint` | Run all linters: `vet` + `go-fmt` + `go-fix` + `go-lint` + `npm-lint` |
|
||||
| `make fmt` | Format Go code (`go fmt ./...`) |
|
||||
| `make clean` | Remove all build artifacts, `node_modules`, generated files, and coverage |
|
||||
| `make help` | List targets with `##` doc comments |
|
||||
|
||||
## Infrastructure
|
||||
|
||||
| Target | Purpose |
|
||||
|---|---|
|
||||
| `make stack-up` | Start Docker Compose infra (Postgres, Pebble, Keycloak, etc.) |
|
||||
| `make stack-down` | Stop Docker Compose infra |
|
||||
| `make stack-ps` | List running containers |
|
||||
| `make psql` | Open a `psql` shell to the dev Postgres database |
|
||||
|
||||
## Codegen
|
||||
|
||||
`make generate` runs all code generation (GraphQL + MCP + Relay). Individual codegen is driven by `go generate`:
|
||||
|
||||
- `go generate ./pkg/server/api/console/v1` — Console GraphQL (gqlgen)
|
||||
- `go generate ./pkg/server/api/connect/v1` — Connect GraphQL (gqlgen)
|
||||
- `go generate ./pkg/server/api/trust/v1` — Trust GraphQL (gqlgen)
|
||||
- `go generate ./pkg/server/api/mcp/v1` — MCP (mcpgen)
|
||||
- `go generate ./pkg/llm` — LLM model registry from OpenRouter (`make genmodels`)
|
||||
|
||||
`make relay` merges split `.graphql` schema files and runs `relay-compiler`.
|
||||
|
||||
## Coverage
|
||||
|
||||
| Target | Purpose |
|
||||
|---|---|
|
||||
| `make coverage-report` | Unit test HTML coverage report (`coverage.html`) |
|
||||
| `make test-e2e-coverage` | E2E coverage report (`coverage-e2e.html`) |
|
||||
| `make coverage-combined` | Combined unit + e2e report (`coverage-combined.html`) |
|
||||
|
||||
## Docker
|
||||
|
||||
| Target | Purpose |
|
||||
|---|---|
|
||||
| `make docker-build` | Build the Docker image (`ghcr.io/getprobo/probo`) |
|
||||
| `make sbom` | Source SBOM (CycloneDX) |
|
||||
| `make sbom-docker` | Docker image SBOM |
|
||||
| `make scan` | Vulnerability scan (Grype) on source + Docker |
|
||||
| `make scan-license` | License compliance scan (Trivy) |
|
||||
|
||||
## Sandbox (Lima)
|
||||
|
||||
| Target | Purpose |
|
||||
|---|---|
|
||||
| `make sandbox-create` | Create a Lima sandbox VM for this worktree |
|
||||
| `make sandbox-start` | Start the VM |
|
||||
| `make sandbox-stop` | Stop (hibernate) the VM |
|
||||
| `make sandbox-delete` | Delete the VM |
|
||||
| `make sandbox-ssh` | Open a shell in the VM |
|
||||
| `make sandbox-status` | Show VM status and IP |
|
||||
|
||||
## Overridable variables
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `SKIP_APPS` | (unset) | Set to `1` to skip frontend app builds |
|
||||
| `CGO_ENABLED` | `0` | Enable/disable CGO |
|
||||
| `GOOS` | (host) | Cross-compile target OS |
|
||||
| `TEST_FLAGS` | `-race -cover -coverprofile=coverage.out` | Extra flags passed to `go test` |
|
||||
| `DOCKER_BUILD_FLAGS` | (empty) | Extra flags for `docker build` |
|
||||
| `E2E_CONFIG` | `e2e/console/testdata/config.yaml` | E2E test config path |
|
||||
221
contrib/claude/n8n.md
Normal file
221
contrib/claude/n8n.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# n8n Node (`packages/n8n-node`)
|
||||
|
||||
Community node package `@probo/n8n-nodes-probo` exposing the Probo API as n8n operations. One `Probo` node with many resources; each resource maps to a set of GraphQL operations against the Console or Connect API.
|
||||
|
||||
## Directory structure
|
||||
|
||||
```
|
||||
packages/n8n-node/
|
||||
credentials/ProboApi.credentials.ts # API key credential (Bearer token)
|
||||
nodes/Probo/
|
||||
Probo.node.ts # Node class — resource picker, dispatch
|
||||
Probo.node.json # n8n codex metadata
|
||||
GenericFunctions.ts # GraphQL request helpers, pagination
|
||||
actions/
|
||||
index.ts # Resource registry, dispatch, field aggregators
|
||||
<resource>/
|
||||
index.ts # Operation dropdown + spread descriptions + re-exports
|
||||
create.operation.ts # One file per operation
|
||||
get.operation.ts
|
||||
getAll.operation.ts
|
||||
update.operation.ts
|
||||
delete.operation.ts
|
||||
...
|
||||
```
|
||||
|
||||
## Resource registration
|
||||
|
||||
Two places must be updated when adding a resource:
|
||||
|
||||
**1. `actions/index.ts`** — import the module and add it to the `resources` map:
|
||||
|
||||
```typescript
|
||||
import * as myresource from './myresource';
|
||||
|
||||
export const resources: Record<string, ResourceModule> = {
|
||||
// ... existing resources ...
|
||||
myresource: myresource as ResourceModule,
|
||||
};
|
||||
```
|
||||
|
||||
**2. `Probo.node.ts`** — add a Resource dropdown entry in the `properties` array:
|
||||
|
||||
```typescript
|
||||
{
|
||||
name: 'My Resource',
|
||||
value: 'myresource',
|
||||
description: 'Manage my resources',
|
||||
},
|
||||
```
|
||||
|
||||
The `value` must match the key in `resources` and the `displayOptions.show.resource` in every operation file.
|
||||
|
||||
## Per-resource file pattern
|
||||
|
||||
### `<resource>/index.ts`
|
||||
|
||||
1. Import each `*.operation.ts` as a namespace.
|
||||
2. Export `description` — the operation dropdown (gated with `displayOptions.show.resource`) plus all spread operation descriptions.
|
||||
3. Re-export each operation module with a name matching its `operation` value.
|
||||
|
||||
```typescript
|
||||
import * as createOp from './create.operation';
|
||||
import * as getOp from './get.operation';
|
||||
import * as getAllOp from './getAll.operation';
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['myresource'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'Create',
|
||||
value: 'create',
|
||||
description: 'Create a new resource',
|
||||
action: 'Create a resource',
|
||||
},
|
||||
// ... more operations ...
|
||||
],
|
||||
default: 'create',
|
||||
},
|
||||
...createOp.description,
|
||||
...getOp.description,
|
||||
...getAllOp.description,
|
||||
];
|
||||
|
||||
export {
|
||||
createOp as create,
|
||||
getOp as get,
|
||||
getAllOp as getAll,
|
||||
};
|
||||
```
|
||||
|
||||
Export names (`create`, `get`, `getAll`, etc.) **must match** the operation `value` strings — `getExecuteFunction` uses them as keys.
|
||||
|
||||
### `<resource>/<verb>.operation.ts`
|
||||
|
||||
Each file exports `description` (field definitions) and `execute` (the handler):
|
||||
|
||||
```typescript
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Organization ID',
|
||||
name: 'organizationId',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['myresource'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
required: true,
|
||||
},
|
||||
// ... more fields ...
|
||||
{
|
||||
displayName: 'Additional Fields',
|
||||
name: 'additionalFields',
|
||||
type: 'collection',
|
||||
placeholder: 'Add Field',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['myresource'],
|
||||
operation: ['create'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
// optional field definitions
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
itemIndex: number,
|
||||
): Promise<INodeExecutionData> {
|
||||
const organizationId = this.getNodeParameter('organizationId', itemIndex) as string;
|
||||
const name = this.getNodeParameter('name', itemIndex) as string;
|
||||
|
||||
const query = `
|
||||
mutation CreateMyResource($input: CreateMyResourceInput!) {
|
||||
createMyResource(input: $input) {
|
||||
myResourceEdge {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const responseData = await proboApiRequest.call(this, query, {
|
||||
input: { organizationId, name },
|
||||
});
|
||||
|
||||
return {
|
||||
json: responseData,
|
||||
pairedItem: { item: itemIndex },
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## GraphQL helpers
|
||||
|
||||
All helpers live in `GenericFunctions.ts`.
|
||||
|
||||
| Helper | API endpoint | Use case |
|
||||
|--------|-------------|----------|
|
||||
| `proboApiRequest` | `/api/console/v1/graphql` | Single mutations and queries |
|
||||
| `proboConnectApiRequest` | `/api/connect/v1/graphql` | Organization/user operations (IAM) |
|
||||
| `proboApiRequestAllItems` | Console API | Cursor-paginated list queries |
|
||||
| `proboConnectApiRequestAllItems` | Connect API | Cursor-paginated list queries (IAM) |
|
||||
| `proboApiMultipartRequest` | Console API | File upload mutations (multipart/form-data) |
|
||||
|
||||
### Pagination (`proboApiRequestAllItems`)
|
||||
|
||||
Caller supplies a `getConnection` function that navigates from the raw GraphQL response to the Relay connection object (must have `edges` and `pageInfo`):
|
||||
|
||||
```typescript
|
||||
const items = await proboApiRequestAllItems.call(
|
||||
this,
|
||||
query,
|
||||
{ organizationId },
|
||||
(response) => {
|
||||
const data = response?.data as IDataObject | undefined;
|
||||
const node = data?.node as IDataObject | undefined;
|
||||
return node?.myResources as IDataObject | undefined;
|
||||
},
|
||||
returnAll,
|
||||
limit,
|
||||
);
|
||||
```
|
||||
|
||||
Internal page size is 100. When `returnAll` is false, stops at `limit`.
|
||||
|
||||
### Update pattern
|
||||
|
||||
For nullable fields, empty string means "clear the value":
|
||||
|
||||
```typescript
|
||||
if (additionalFields.description !== undefined) {
|
||||
input.description = additionalFields.description === '' ? null : additionalFields.description;
|
||||
}
|
||||
```
|
||||
|
||||
## Adding a new resource — checklist
|
||||
|
||||
1. **Directory** — create `nodes/Probo/actions/<resource>/` with `index.ts` and one `*.operation.ts` per operation
|
||||
2. **Operations** — each file exports `description` (fields gated with `displayOptions`) and `execute` (reads params, calls GraphQL, returns `{ json, pairedItem }`)
|
||||
3. **Index** — `<resource>/index.ts` defines the operation dropdown, spreads all descriptions, re-exports ops with matching value names
|
||||
4. **Register** — import and add to `resources` map in `actions/index.ts`
|
||||
5. **Node** — add Resource dropdown entry in `Probo.node.ts` properties
|
||||
6. **Verify** — `npx n8n-node lint` must pass
|
||||
Reference in New Issue
Block a user