Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/loose-lamps-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@permify-toolkit/core": minor
"@permify-toolkit/cli": minor
---

Add relationships list and export CLI commands for querying and exporting relationship tuples from a Permify tenant. Includes new readRelationships core function with pagination support, shared filter flag helpers, and two output formats (table and compact).
11 changes: 2 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,18 +249,11 @@ pnpm test -- --files client.spec.ts

We're actively working on expanding the toolkit. Here's what's coming:

- [x] Authorization guards for NestJS package
- [x] Full-stack example app (frontend + NestJS backend)
- [x] Release v1.0.0 and publish to npm
- [x] Multi permission checks (AND + OR logic)
- [x] Docs
- [ ] Testing utilities — mock Permify client and test helpers for unit testing authorization logic
- [x] Schema validation CLI command
- [x] Relationship query CLI commands — list, inspect, and export existing relationships from a tenant
- [ ] Permission result caching — in-memory (and optionally Redis-backed) cache to reduce gRPC round-trips
- [x] Schema validation CLI command — lint and validate schema syntax before pushing to Permify
- [ ] Relationship query CLI commands — list, inspect, and export existing relationships from a tenant
- [ ] Express.js / Fastify middleware — permission-check middleware for non-NestJS backends
- [ ] GraphQL support — `@CheckPermission` directive and guards for NestJS GraphQL resolvers
- [ ] ABAC helpers — high-level utilities for attribute-based access control rules
- [ ] OpenTelemetry tracing — structured spans and metrics for permission checks and schema operations
- [ ] Schema diff CLI command — preview what will change before pushing a schema update
- [ ] Multi-tenant CLI management — create, list, and delete tenants directly from the CLI
Expand Down
166 changes: 165 additions & 1 deletion docs-site/docs/packages/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ sidebar_position: 3

# @permify-toolkit/cli

CLI for pushing schemas and seeding relationships to your Permify instance.
CLI for managing schemas, seeding relationships, and querying relationship data from your Permify instance.

[![NPM Version](https://img.shields.io/npm/v/@permify-toolkit/cli)](https://www.npmjs.com/package/@permify-toolkit/cli)

Expand Down Expand Up @@ -234,6 +234,170 @@ permify-toolkit relationships seed --tenant my-tenant-id --file-path ./data/rela
permify-toolkit relationships seed --tenant new-tenant-id --file-path ./relationships.json --create-tenant
```

### `relationships list`

Queries and displays relationship tuples from a Permify tenant. Useful for debugging authorization — quickly see what relationships exist for a given entity type.

Unlike `schema push` or `relationships seed`, this command **does not require a schema** in your config. It only needs a `client` connection.

```bash
permify-toolkit relationships list --entity-type <type> [--tenant <id>] [flags]
```

**Flags:**

| Flag | Alias | Description | Required | Default |
| ---------------- | ----- | ----------------------------------- | -------- | ----------- |
| `--entity-type` | `-e` | Entity type to query | Yes | |
| `--tenant` | | Tenant ID | No | From config |
| `--entity-id` | | Filter by a specific entity ID | No | |
| `--relation` | `-r` | Filter by relation name | No | |
| `--subject-type` | `-s` | Filter by subject type | No | |
| `--subject-id` | | Filter by subject ID | No | |
| `--output` | `-o` | Output format: `table` or `compact` | No | `table` |
| `--page-size` | `-p` | Number of results per gRPC page | No | `50` |

**Minimal config required:**

```typescript
// permify.config.ts — no schema needed for read-only commands
export default {
tenant: "t1",
client: {
endpoint: "localhost:3478",
insecure: true
}
};
```

**Examples:**

```bash
# List all relationships for the "document" entity type
permify-toolkit relationships list -e document

# Filter by relation and subject
permify-toolkit relationships list -e document -r viewer -s user

# Show a specific entity's relationships
permify-toolkit relationships list -e document --entity-id doc-1

# Use compact output (one tuple per line)
permify-toolkit relationships list -e document -o compact

# Specify tenant and page size
permify-toolkit relationships list -e document --tenant my-tenant -p 100
```

**Output formats:**

Table (default):

```
Entity Type Entity ID Relation Subject Type Subject ID Subject Relation
document doc-1 owner user alice
document doc-1 viewer user bob
document doc-2 viewer group eng member
✔ Found 3 relationships
```

Compact (one Zanzibar-style tuple per line):

```
document:doc-1#owner@user:alice
document:doc-1#viewer@user:bob
document:doc-2#viewer@group:eng#member
✔ Found 3 relationships
```

When no relationships match:

```
ℹ No relationships found.
```

:::tip Debugging permissions
Use `relationships list` to verify that the tuples you expect actually exist in Permify. If a permission check fails unexpectedly, list the relationships for that entity to see what's stored.
:::

### `relationships export`

Exports relationship tuples from a Permify tenant to a JSON file. The output format is identical to the `relationships seed` input format — so you can export from one tenant and seed into another.

Like `relationships list`, this command **does not require a schema** in your config.

```bash
permify-toolkit relationships export --entity-type <type> --file-path <path> [--tenant <id>] [flags]
```

**Flags:**

| Flag | Alias | Description | Required | Default |
| ---------------- | ----- | ---------------------------------- | -------- | ----------- |
| `--entity-type` | `-e` | Entity type to query | Yes | |
| `--file-path` | `-f` | Output file path (must be `.json`) | Yes | |
| `--tenant` | | Tenant ID | No | From config |
| `--entity-id` | | Filter by a specific entity ID | No | |
| `--relation` | `-r` | Filter by relation name | No | |
| `--subject-type` | `-s` | Filter by subject type | No | |
| `--subject-id` | | Filter by subject ID | No | |
| `--page-size` | `-p` | Number of results per gRPC page | No | `100` |

**Examples:**

```bash
# Export all document relationships to a file
permify-toolkit relationships export -e document -f ./backup/documents.json

# Export only viewer relationships
permify-toolkit relationships export -e document -r viewer -f viewers.json

# Export from a specific tenant
permify-toolkit relationships export -e document --tenant staging -f staging-docs.json
```

**Output file format:**

The exported JSON file uses the same structure as `relationships seed`, so you can directly re-import it:

```json
{
"tuples": [
{
"entity": { "type": "document", "id": "doc-1" },
"relation": "owner",
"subject": { "type": "user", "id": "alice" }
},
{
"entity": { "type": "document", "id": "doc-1" },
"relation": "viewer",
"subject": { "type": "user", "id": "bob" }
}
]
}
```

**Common workflows:**

```bash
# Back up relationships before a migration
permify-toolkit relationships export -e document -f ./backup/documents.json
permify-toolkit relationships export -e organization -f ./backup/orgs.json

# Copy relationships from staging to dev
permify-toolkit relationships export -e document --tenant staging -f transfer.json
permify-toolkit relationships seed --tenant dev -f transfer.json

# Export, inspect, edit, then re-seed
permify-toolkit relationships export -e document -f tuples.json
# ... edit tuples.json manually ...
permify-toolkit relationships seed --tenant t1 -f tuples.json
```

:::tip Export + Seed round-trip
The export format is designed to be seed-compatible. You can export from one environment, review or modify the JSON, and seed it into another — making it easy to migrate or duplicate relationship data across tenants.
:::

## Local Development

```bash
Expand Down
69 changes: 69 additions & 0 deletions docs-site/docs/packages/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,74 @@ await writeRelationships(client, {
});
```

### Reading Relationships

Query existing relationship tuples from a tenant. Handles pagination automatically — all matching tuples are returned in a single array.

```typescript
import { readRelationships } from "@permify-toolkit/core";

const tuples = await readRelationships({
client,
tenantId: "my-tenant",
filter: {
entity: { type: "document" }
}
});

// tuples = [
// { entity: { type: "document", id: "doc-1" }, relation: "owner", subject: { type: "user", id: "alice" } },
// { entity: { type: "document", id: "doc-1" }, relation: "viewer", subject: { type: "user", id: "bob" } },
// ...
// ]
```

**Filter options:**

```typescript
// Filter by entity type only (returns all relationships for that type)
await readRelationships({
client,
tenantId: "t1",
filter: { entity: { type: "document" } }
});

// Filter by specific entity
await readRelationships({
client,
tenantId: "t1",
filter: { entity: { type: "document", ids: ["doc-1"] } }
});

// Filter by relation
await readRelationships({
client,
tenantId: "t1",
filter: {
entity: { type: "document" },
relation: "viewer"
}
});

// Filter by subject
await readRelationships({
client,
tenantId: "t1",
filter: {
entity: { type: "document" },
subject: { type: "user", ids: ["alice"] }
}
});

// Control page size for large datasets
await readRelationships({
client,
tenantId: "t1",
filter: { entity: { type: "document" } },
pageSize: 100 // default: 50
});
```

### Deleting Relationships

```typescript
Expand Down Expand Up @@ -156,6 +224,7 @@ export default defineConfig({
| `clientOptionsFromEnv()` | Read client options from env vars |
| `checkPermission()` | Check a permission |
| `writeRelationships()` | Write relationship tuples |
| `readRelationships()` | Read relationship tuples with filtering and automatic pagination |
| `deleteRelationships()` | Delete relationship tuples |
| `relationsOf()` | Helper to extract relations from schema |
| `getSchemaWarnings()` | Collect non-blocking warnings from a schema AST (unused relations, empty entities, missing permissions) |
2 changes: 1 addition & 1 deletion packages/cli/permify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
} from "@permify-toolkit/core";

export default defineConfig({
tenant: "t1",
tenant: "toolkit-test",
client: {
endpoint: "localhost:3478",
insecure: true
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ export abstract class BaseCommand extends Command {
return { client, config };
}

protected async clientFromConfigLite(): Promise<{
client: any;
config: Config;
}> {
const config = await loadConfig(undefined, { skipSchemaValidation: true });

if (!config.client?.endpoint) {
this.error("Client endpoint not defined in config");
}

const client = createPermifyClient(config.client);
return { client, config };
}

/**
* Resolves the tenant ID from CLI flags or config.
* Priority: CLI flag > config file > error
Expand Down
61 changes: 61 additions & 0 deletions packages/cli/src/commands/relationships/export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import fsNode from "node:fs";
import path from "node:path";
import { Flags } from "@oclif/core";
import { readRelationships } from "@permify-toolkit/core";

import { BaseCommand } from "../../base.js";
import { relationshipFilterFlags, buildTupleFilter } from "../../helpers.js";

export default class RelationshipExport extends BaseCommand {
static description =
"Export relationships from a Permify tenant to a JSON file";

static flags = {
...BaseCommand.baseFlags,
...relationshipFilterFlags,
"file-path": Flags.string({
char: "f",
description: "Output file path (.json)",
required: true
}),
"page-size": Flags.integer({
char: "p",
description: "Results per page",
default: 100
})
};

async run() {
const { flags } = await this.parse(RelationshipExport);

// Validate file path
const filePath = path.resolve(flags["file-path"]);
if (!filePath.toLowerCase().endsWith(".json")) {
this.error("Output file must have a .json extension");
}

const dir = path.dirname(filePath);
if (!fsNode.existsSync(dir)) {
this.error(`Output directory does not exist: ${dir}`);
}

const { client, config } = await this.clientFromConfigLite();
const tenantId = this.resolveTenant(flags, config);

const filter = buildTupleFilter(flags);

const relationships = await readRelationships({
client,
tenantId,
filter,
pageSize: flags["page-size"]
});

const output = JSON.stringify({ tuples: relationships }, null, 2);
fsNode.writeFileSync(filePath, output, "utf-8");

this.log(
`✔ Exported ${relationships.length} relationships to ${flags["file-path"]}`
);
}
}
Loading
Loading