CLI: Add remote API commands for Data API and Metadata API#1059
CLI: Add remote API commands for Data API and Metadata API#1059
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/205a6eab-27f7-46cf-aee8-b628c23b3490 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/205a6eab-27f7-46cf-aee8-b628c23b3490 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/205a6eab-27f7-46cf-aee8-b628c23b3490 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
|
@anthropic-code-agent fix all ci build and test errors |
|
…k.yaml Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/cc3d4634-2e71-4d72-ab01-bf11a371eef3 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Fixed in commit
|
There was a problem hiding this comment.
Pull request overview
This PR expands the ObjectStack CLI with a set of remote-first commands that talk to a running ObjectStack server over the REST APIs, including auth, data CRUD/query, and metadata management, plus shared CLI utilities and tests.
Changes:
- Added 12 new remote API CLI commands under
auth,data, andmetatopics. - Introduced shared utilities for API client creation, credential storage, and output formatting (json/table/yaml).
- Added documentation, unit tests for command conventions/utilities, and updated CLI dependencies + changelog.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 19 comments.
Show a summary per file
| File | Description |
|---|---|
| REMOTE_API_COMMANDS.md | End-user documentation for remote API CLI workflows and examples. |
| packages/cli/src/utils/api-client.ts | Creates @objectstack/client instances and enforces auth presence. |
| packages/cli/src/utils/auth-config.ts | Reads/writes/deletes ~/.objectstack/credentials.json. |
| packages/cli/src/utils/output-formatter.ts | Implements json / yaml / table output formatting. |
| packages/cli/src/commands/auth/login.ts | Adds interactive/non-interactive login and credential persistence. |
| packages/cli/src/commands/auth/logout.ts | Clears stored credentials. |
| packages/cli/src/commands/auth/whoami.ts | Calls session endpoint and prints current user/session info. |
| packages/cli/src/commands/data/query.ts | Queries records with filter/fields/sort/limit/offset and formatting. |
| packages/cli/src/commands/data/get.ts | Fetches a single record by ID with formatting. |
| packages/cli/src/commands/data/create.ts | Creates a record from JSON arg or JSON file. |
| packages/cli/src/commands/data/update.ts | Partially updates a record from JSON arg or JSON file. |
| packages/cli/src/commands/data/delete.ts | Deletes a record with json/table output. |
| packages/cli/src/commands/meta/list.ts | Lists metadata types or items with json/table/yaml output. |
| packages/cli/src/commands/meta/get.ts | Fetches a metadata item with json/table/yaml output. |
| packages/cli/src/commands/meta/register.ts | Registers metadata from a JSON file (PUT). |
| packages/cli/src/commands/meta/delete.ts | Deletes metadata (currently via direct fetch). |
| packages/cli/test/remote-api-utils.test.ts | Unit tests for api-client/auth-config/output formatter utilities. |
| packages/cli/test/remote-api-commands.test.ts | Convention/shape tests for the new oclif commands. |
| packages/cli/package.json | Adds workspace dependency on @objectstack/client and yaml. |
| CHANGELOG.md | Documents the new remote API commands and their flags/formats. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
packages/cli/src/utils/api-client.ts
Outdated
| // Resolve server URL | ||
| const baseUrl = options.url || | ||
| process.env.OBJECTSTACK_URL || | ||
| 'http://localhost:3000'; | ||
|
|
||
| // Resolve authentication token | ||
| let token = options.token || process.env.OBJECTSTACK_TOKEN; | ||
|
|
||
| // If no token provided via options or env, try to load from stored credentials | ||
| if (!token) { | ||
| try { | ||
| const authConfig = await readAuthConfig(); | ||
| token = authConfig.token; | ||
| } catch { | ||
| // No stored credentials - commands will fail if auth is required | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
createApiClient() never uses the stored credentials URL (authConfig.url). After os auth login --url ..., running commands without --url will still default to OBJECTSTACK_URL or http://localhost:3000, which contradicts the documented priority order and breaks the “stored credentials” workflow. Consider loading authConfig when token is missing (or when url is missing) and using authConfig.url as the fallback before defaulting to localhost.
| // Resolve server URL | |
| const baseUrl = options.url || | |
| process.env.OBJECTSTACK_URL || | |
| 'http://localhost:3000'; | |
| // Resolve authentication token | |
| let token = options.token || process.env.OBJECTSTACK_TOKEN; | |
| // If no token provided via options or env, try to load from stored credentials | |
| if (!token) { | |
| try { | |
| const authConfig = await readAuthConfig(); | |
| token = authConfig.token; | |
| } catch { | |
| // No stored credentials - commands will fail if auth is required | |
| } | |
| } | |
| // Resolve server URL (without applying defaults yet) | |
| let baseUrl = options.url || process.env.OBJECTSTACK_URL; | |
| // Resolve authentication token | |
| let token = options.token || process.env.OBJECTSTACK_TOKEN; | |
| // If URL or token is missing, try to load from stored credentials | |
| if (!baseUrl || !token) { | |
| try { | |
| const authConfig = await readAuthConfig(); | |
| if (!token && authConfig.token) { | |
| token = authConfig.token; | |
| } | |
| if (!baseUrl && (authConfig as any).url) { | |
| baseUrl = (authConfig as any).url; | |
| } | |
| } catch { | |
| // No stored credentials - commands will fail if auth is required | |
| } | |
| } | |
| // Apply final default for baseUrl if still not resolved | |
| if (!baseUrl) { | |
| baseUrl = 'http://localhost:3000'; | |
| } |
| import { ObjectStackClient } from '@objectstack/client'; | ||
| import { readAuthConfig } from './auth-config.js'; | ||
|
|
There was a problem hiding this comment.
This module imports AuthConfig but never uses it. Removing the unused import avoids lint noise and keeps the file focused.
| // Resolve authentication token | ||
| let token = options.token || process.env.OBJECTSTACK_TOKEN; | ||
|
|
||
| // If no token provided via options or env, try to load from stored credentials | ||
| if (!token) { | ||
| try { | ||
| const authConfig = await readAuthConfig(); | ||
| token = authConfig.token; | ||
| } catch { | ||
| // No stored credentials - commands will fail if auth is required | ||
| } | ||
| } | ||
|
|
||
| // Create and return the client | ||
| return new ObjectStackClient({ | ||
| baseUrl, | ||
| token, | ||
| debug: options.debug || false, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Many commands call requireAuth((client as any).token) because ObjectStackClient.token is a private field. This couples the CLI to internal @objectstack/client implementation details and may break on refactors. A more robust approach is to have createApiClient() return the resolved token (or expose a public getter on the client) so requireAuth can validate without as any casts.
| // Ensure directory exists | ||
| await mkdir(dir, { recursive: true }); | ||
|
|
||
| // Write credentials file | ||
| await writeFile(path, JSON.stringify(config, null, 2), { mode: 0o600 }); | ||
| } |
There was a problem hiding this comment.
writeAuthConfig() writes with { mode: 0o600 }, but if the credentials file already exists, Node won’t necessarily update permissions. That can leave credentials readable if the file previously had broader perms. Consider explicitly chmod’ing to 0600 after write (best-effort, platform-aware) to enforce the security guarantee.
| if (!password) { | ||
| // Note: This doesn't hide the password input in the terminal | ||
| // For production use, consider using a library like 'inquirer' or 'prompts' | ||
| password = await rl.question('Password: '); | ||
| } |
There was a problem hiding this comment.
Interactive password input is echoed to the terminal (readline.question('Password: ')). This exposes passwords in terminal scrollback/screen recordings and is generally unsafe for an auth command. Consider switching to a prompt implementation that supports masked/hidden input (or implement hidden input with readline keypress handling).
| import { Args, Command, Flags } from '@oclif/core'; | ||
| import { printError, printSuccess } from '../../utils/format.js'; | ||
| import { createApiClient, requireAuth } from '../../utils/api-client.js'; | ||
| import { formatOutput } from '../../utils/output-formatter.js'; | ||
|
|
There was a problem hiding this comment.
printHeader is imported but never used in this command. Please remove the unused import to avoid lint warnings and keep the module tidy.
| // Note: The current client doesn't have a direct delete method for metadata | ||
| // We'll need to use fetch directly with the proper endpoint | ||
| const baseUrl = (client as any).baseUrl; | ||
| const token = (client as any).token; | ||
|
|
||
| const response = await fetch(`${baseUrl}/api/v1/meta/${encodeURIComponent(args.type)}/${encodeURIComponent(args.name)}`, { | ||
| method: 'DELETE', | ||
| headers: { | ||
| 'Authorization': `Bearer ${token}`, | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorBody = await response.text().catch(() => 'Unknown error'); | ||
| throw new Error(`Delete failed (${response.status}): ${errorBody}`); | ||
| } | ||
|
|
There was a problem hiding this comment.
This command bypasses @objectstack/client and hard-codes a DELETE request using fetch, while also reaching into private client fields ((client as any).baseUrl/token). That loses the client’s standardized error handling (and discovered route support) and increases maintenance cost. Prefer adding a client.meta.deleteItem(type, name) API to @objectstack/client (or exposing a safe request helper) and using that here.
| // Note: The current client doesn't have a direct delete method for metadata | |
| // We'll need to use fetch directly with the proper endpoint | |
| const baseUrl = (client as any).baseUrl; | |
| const token = (client as any).token; | |
| const response = await fetch(`${baseUrl}/api/v1/meta/${encodeURIComponent(args.type)}/${encodeURIComponent(args.name)}`, { | |
| method: 'DELETE', | |
| headers: { | |
| 'Authorization': `Bearer ${token}`, | |
| 'Content-Type': 'application/json', | |
| }, | |
| }); | |
| if (!response.ok) { | |
| const errorBody = await response.text().catch(() => 'Unknown error'); | |
| throw new Error(`Delete failed (${response.status}): ${errorBody}`); | |
| } | |
| // Delegate to the client metadata API so we use standardized routing and error handling | |
| const metaClient = (client as any).meta; | |
| if (!metaClient || typeof metaClient.deleteItem !== 'function') { | |
| throw new Error('Metadata delete is not supported by the current client version.'); | |
| } | |
| await metaClient.deleteItem(args.type, args.name); |
| import { Args, Command, Flags } from '@oclif/core'; | ||
| import { printError, printSuccess } from '../../utils/format.js'; | ||
| import { createApiClient, requireAuth } from '../../utils/api-client.js'; | ||
|
|
There was a problem hiding this comment.
printHeader is imported but never used in this command. Please remove the unused import to avoid lint warnings and keep the module tidy.
| - All commands support `--url` and `--token` flags, or use stored credentials from `~/.objectstack/credentials.json` | ||
| - Multiple output formats: `--format json|table|yaml` | ||
| - Environment variable support: `OBJECTSTACK_URL`, `OBJECTSTACK_TOKEN` |
There was a problem hiding this comment.
The CHANGELOG claims multiple output formats --format json|table|yaml, but several new commands only allow json|table (e.g., data delete, meta register, meta delete). Either expand those commands to support yaml or adjust this entry to reflect the actual supported formats to avoid misleading users.
| const mockUnlink = vi.fn().mockResolvedValue(undefined); | ||
| vi.doMock('node:fs/promises', () => ({ | ||
| unlink: mockUnlink, | ||
| })); | ||
|
|
||
| await deleteAuthConfig(); | ||
|
|
||
| // Should not throw | ||
| }); | ||
|
|
||
| it('should not throw if file does not exist', async () => { | ||
| const mockUnlink = vi.fn().mockRejectedValue({ code: 'ENOENT' }); | ||
| vi.doMock('node:fs/promises', () => ({ | ||
| unlink: mockUnlink, | ||
| })); | ||
|
|
||
| await expect(deleteAuthConfig()).resolves.not.toThrow(); |
There was a problem hiding this comment.
The deleteAuthConfig tests use vi.doMock('node:fs/promises', ...) after node:fs/promises has already been imported/mocked at the top of the file. This doesn’t reliably affect the module instance used by deleteAuthConfig()’s dynamic import, and the tests currently don’t assert that unlink was called or that ENOENT is handled. Prefer configuring vi.mocked(fs.unlink) directly (and asserting calls) or using vi.resetModules() + re-import to ensure the mock applies.
| const mockUnlink = vi.fn().mockResolvedValue(undefined); | |
| vi.doMock('node:fs/promises', () => ({ | |
| unlink: mockUnlink, | |
| })); | |
| await deleteAuthConfig(); | |
| // Should not throw | |
| }); | |
| it('should not throw if file does not exist', async () => { | |
| const mockUnlink = vi.fn().mockRejectedValue({ code: 'ENOENT' }); | |
| vi.doMock('node:fs/promises', () => ({ | |
| unlink: mockUnlink, | |
| })); | |
| await expect(deleteAuthConfig()).resolves.not.toThrow(); | |
| const mockUnlink = vi.mocked(fs.unlink); | |
| mockUnlink.mockResolvedValue(undefined as any); | |
| await deleteAuthConfig(); | |
| expect(mockUnlink).toHaveBeenCalled(); | |
| expect(mockUnlink).toHaveBeenCalledWith( | |
| expect.stringContaining('credentials.json') | |
| ); | |
| }); | |
| it('should not throw if file does not exist', async () => { | |
| const mockUnlink = vi.mocked(fs.unlink); | |
| mockUnlink.mockRejectedValue({ code: 'ENOENT' } as any); | |
| await expect(deleteAuthConfig()).resolves.not.toThrow(); | |
| expect(mockUnlink).toHaveBeenCalled(); |
|
@copilot fix all |
…chmod, password masking, yaml formats, tests Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/cbe33622-849e-44e4-a736-3622d7348646 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/cbe33622-849e-44e4-a736-3622d7348646 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
All review feedback addressed in commits
|
|
@copilot 16:52:28.520 @objectstack/spec:build: ✓ data/TenantDatabaseLifecycle.json |
… auth/login.ts Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/a0bd0f1a-f742-48c7-a74e-41aed3aecf26 Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Fixed in commit |
The CLI had 16+ local commands but only 1 remote API call (
plugin publish). This adds 12 P0 commands to interact with running ObjectStack servers via REST APIs.Commands Added
Authentication (3)
os auth login- Store credentials in~/.objectstack/credentials.json(mode 0600)os auth logout- Clear stored credentialsos auth whoami- Show current session viaGET /auth/get-sessionData API (5)
os data query <object>- Query with filters, sorting, paginationos data get <object> <id>- Fetch single recordos data create <object>- Create from JSON string or fileos data update <object> <id>- Partial updateos data delete <object> <id>- Delete recordMetadata API (4)
os meta list [type]- List types or items of a typeos meta get <type> <name>- Fetch metadata definitionos meta register <type>- Register from JSON fileos meta delete <type> <name>- Delete metadata itemInfrastructure
Common utilities
utils/api-client.ts- Factory for@objectstack/clientwith auth resolutionutils/auth-config.ts- Credential storage managementutils/output-formatter.ts- JSON/table/YAML formattingShared patterns
--url/OBJECTSTACK_URLfor server configuration (default:http://localhost:3000)--token/OBJECTSTACK_TOKENfor auth, or read from stored credentials--format json|table|yamlfor output formattingExample Usage
Testing
Dependencies
@objectstack/clientworkspace dependencyyamlfor YAML output support