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
44 changes: 11 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,49 +95,27 @@ const client = new SuperOpsClient({
```typescript
// Get single asset
const asset = await client.assets.get('asset-123');
console.log(asset.name, asset.hostName, asset.status);

// List assets with filtering
const result = await client.assets.list({
first: 50,
filter: {
status: 'ACTIVE',
type: 'WORKSTATION',
clientId: 'client-456',
},
orderBy: {
field: 'NAME',
direction: 'ASC',
},
});
// List assets, one page at a time
const result = await client.assets.list({ page: 1, pageSize: 50 });
console.log(result.items, result.meta.totalCount);

// Auto-paginate all assets
for await (const asset of client.assets.listAll()) {
console.log(asset.name);
}

// List by client
const clientAssets = await client.assets.listByClient('client-456');

// List by site
const siteAssets = await client.assets.listBySite('site-789');

// Create asset
const newAsset = await client.assets.create({
name: 'New Workstation',
type: 'WORKSTATION',
clientId: 'client-456',
// Update an asset's custom fields
const updated = await client.assets.update('asset-123', {
customFields: { location: 'HQ' },
});

// Update asset
const updatedAsset = await client.assets.update('asset-123', {
name: 'Updated Name',
status: 'MAINTENANCE',
});

// Delete asset
await client.assets.delete('asset-123');
```

> **Note:** Other resources (`tickets`, `clients`, `sites`, …) are still being
> migrated to the real SuperOps schema — see the v2.0.0 tracking issue. Only
> `assets` is verified against SuperOps' published API today.

### Tickets

```typescript
Expand Down
62 changes: 62 additions & 0 deletions SCHEMA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# SuperOps GraphQL Schema Reference

This document records the SuperOps MSP GraphQL schema as the SDK understands
it. It exists because the SDK was originally written against an **assumed**
schema that did not match the real API (see issue #4) — every resource needs
to be migrated to the real schema, and this file tracks that work.

**Source:** SuperOps public API documentation — <https://developer.superops.com/msp>.

**Endpoints:**

- US: `https://api.superops.ai/msp`
- EU: `https://euapi.superops.ai/msp`

> ⚠️ This reference was derived from public documentation, not from live schema
> introspection. Object-type field names are reliable; **list input shapes
> (`ListInfoInput`) and `ListInfo` field names are best-effort and should be
> confirmed against the live API.** The authoritative way to verify is a
> GraphQL introspection query against the endpoint above with a valid token.

## Pagination model

SuperOps uses **page-based** pagination, not GraphQL cursor connections:

- List queries take a `ListInfoInput!` argument.
- List responses are `<Entity>List { <entities>: [...], listInfo: ListInfo }`.
- `ListInfo` reports the page number, page size, and total record count.

The SDK exposes this as `Page<T> { items: T[]; meta: ListMeta }` and a
`PageParams { page?, pageSize? }` input. See `src/types/common.ts`.

## Assets — migrated ✅

```graphql
getAsset(input: AssetIdentifierInput!): Asset
getAssetList(input: ListInfoInput!): AssetList
updateAsset(input: UpdateAssetInput!): Asset

input AssetIdentifierInput { assetId: String! }

type Asset {
assetId, name, status
assetClass { classId, name }
client { accountId, name }
site { id, name }
requester { userId, name }
primaryMac, loggedInUser, serialNumber, manufacturer, model
hostName, publicIp, gateway, platform, domain, sysUptime
lastCommunicatedTime, agentVersion
platformFamily, platformCategory, platformVersion, patchStatus
warrantyExpiryDate, purchasedDate, lastReportedTime, customFields
}

type AssetList { assets: [Asset!]!, listInfo: ListInfo! }
```

## Other resources — NOT yet migrated ❌

Every other resource (`tickets`, `clients`, `sites`, `alerts`, `contracts`,
`technicians`, `knowledge-base`, …) still uses the original assumed schema and
will fail against the real API. The full query/mutation inventory and the
migration plan are tracked in the v2.0.0 GitHub issue.
74 changes: 73 additions & 1 deletion src/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Cursor-based pagination utilities for SuperOps GraphQL API
*/

import type { AsyncIterableWithHelpers, Connection, PageInfo } from './types/index.js';
import type { AsyncIterableWithHelpers, Connection, Page, PageInfo } from './types/index.js';

/**
* Default page size for pagination
Expand Down Expand Up @@ -108,6 +108,78 @@ export function createCursorPaginatedIterator<T>(
return iterable;
}

/**
* Function type for fetching a single page of a page-based list query
*/
export type PageListFetcher<T> = (params: {
page: number;
pageSize: number;
}) => Promise<Page<T>>;

/**
* Create an async iterable that automatically walks every page of a
* page-based (page/pageSize) SuperOps list query, yielding individual items.
*/
export function createPagePaginatedIterator<T>(
fetcher: PageListFetcher<T>,
options: PaginationOptions = {}
): AsyncIterableWithHelpers<T> {
const pageSize = Math.min(options.pageSize || DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE);
const maxItems = options.maxItems;

let buffer: T[] = [];
let bufferIndex = 0;
let nextPage = 1;
let hasMore = true;
let totalReturned = 0;

const iterator: AsyncIterator<T> = {
async next(): Promise<IteratorResult<T>> {
if (maxItems !== undefined && totalReturned >= maxItems) {
return { done: true, value: undefined };
}

while (bufferIndex >= buffer.length && hasMore) {
const page = await fetcher({ page: nextPage, pageSize });
buffer = page.items;
bufferIndex = 0;

// A short page means there is nothing after it; otherwise compare the
// running count against the reported total.
const fetchedThroughThisPage = nextPage * pageSize;
hasMore =
buffer.length === pageSize && fetchedThroughThisPage < page.meta.totalCount;
nextPage += 1;

if (buffer.length === 0) {
return { done: true, value: undefined };
}
}

if (bufferIndex >= buffer.length) {
return { done: true, value: undefined };
}

const item = buffer[bufferIndex++];
totalReturned++;
return { done: false, value: item };
},
};

return {
[Symbol.asyncIterator](): AsyncIterator<T> {
return iterator;
},
async toArray(): Promise<T[]> {
const results: T[] = [];
for await (const item of this) {
results.push(item);
}
return results;
},
};
}

/**
* Create a page iterator that yields entire connection pages
*/
Expand Down
Loading