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
166 changes: 90 additions & 76 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,65 @@
# Fixle SDK
# @spectorasoftware/fixle-sdk

[![CI](https://github.com/spectorasoftware/fixle-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/spectorasoftware/fixle-sdk/actions/workflows/ci.yml)
[![Docs](https://img.shields.io/badge/docs-JSDoc-blue)](https://spectorasoftware.github.io/fixle-sdk/index.html)

TypeScript SDK for interacting with the Fixle API.
TypeScript SDK for the Fixle API. Manage properties, inspections, and appliances.

## Installation

```bash
npm install @spectora/fixle-sdk
npm install @spectorasoftware/fixle-sdk
```

### Installing from GitHub Packages

Add to your `.npmrc`:

```
@spectorasoftware:registry=https://npm.pkg.github.com
```

Or for local development:
Then install:

```bash
npm install ../fixle-sdk
npm install @spectorasoftware/fixle-sdk
```

## Usage
### Installing from Git

```bash
npm install git+https://github.com/spectorasoftware/fixle-sdk.git
```

## Quick Start

```typescript
import { FixleClient } from '@spectora/fixle-sdk';
import { FixleClient } from '@spectorasoftware/fixle-sdk';

// Initialize the client
const client = new FixleClient({
apiUrl: 'https://fixle-api.example.com',
apiKey: 'your-api-key-here',
apiUrl: 'https://api.fixle.com',
apiKey: 'your-api-key'
});

// Send inspection data to Fixle
const result = await client.sendToFixleApi({
inspection_id: 12345,
address: '123 Main Street, Portland, OR 97201',
appliances: [
{
item_name: 'Water Heater',
section_name: 'Plumbing',
brand: 'Rheem',
model: 'XE50M06ST45U1',
serial_number: 'ABC123456',
manufacturer: 'Rheem Manufacturing',
year: '2020',
},
],
});
// Create a property
const propertyId = await client.findOrCreateProperty('123 Main St, Portland, OR 97201');

console.log(`Created ${result} appliances`);
// Create an inspection
await client.createInspection(propertyId, 12345);

// Create an inspection with inspector image
await client.createInspection(propertyId, 12345, 'https://example.com/inspector.jpg');

// Add an appliance
await client.createAppliance(propertyId, {
item_name: 'Water Heater',
section_name: 'Basement',
brand: 'Rheem',
model: 'XE50M06ST45U1',
serial_number: 'ABC123',
manufacturer: 'Rheem Manufacturing',
year: '2020'
});
```

## API
## API Reference

### `FixleClient`

Expand All @@ -57,63 +69,66 @@ console.log(`Created ${result} appliances`);
new FixleClient(config: FixleClientConfig)
```

**Parameters:**
- `config.apiUrl` (string): Base URL of the Fixle API
- `config.apiKey` (string): API key for authentication
| Parameter | Type | Description |
|-----------|------|-------------|
| `config.apiUrl` | `string` | Base URL of the Fixle API |
| `config.apiKey` | `string` | API key for authentication |

#### Methods

##### `sendToFixleApi(data: ExtractedData): Promise<number>`

Sends inspection data to Fixle API. Creates property, inspection, and appliances.

**Returns:** Number of appliances successfully created

##### `findOrCreateProperty(address: string): Promise<number>`

Finds or creates a property by address.

**Returns:** Property ID
Creates a new property from an address string.

##### `createInspection(propertyId: number, inspectionId: number): Promise<void>`

Creates an inspection for a property.

##### `createPropertyAppliance(propertyId: number, appliance: Appliance): Promise<void>`
```typescript
const propertyId = await client.findOrCreateProperty('123 Main St, Portland, OR 97201');
```

Creates a property appliance.
##### `createInspection(propertyId: number, inspectionId: number, inspectorImageUrl?: string): Promise<void>`

## Types
Creates an inspection record for a property.

```typescript
interface ExtractedData {
inspection_id: number;
address: string;
appliances: Appliance[];
}

interface Appliance {
item_name: string;
section_name: string;
brand: string | null;
model: string | null;
serial_number: string | null;
manufacturer: string | null;
year: string | null;
}
// Without inspector image
await client.createInspection(123, 45678);

// With inspector image
await client.createInspection(123, 45678, 'https://example.com/inspector.jpg');
```

## Documentation
| Parameter | Type | Description |
|-----------|------|-------------|
| `propertyId` | `number` | ID of the property |
| `inspectionId` | `number` | External inspection ID |
| `inspectorImageUrl` | `string` (optional) | URL to inspector's profile image |

Full API documentation is available at: https://spectorasoftware.github.io/fixle-sdk/index.html
##### `createAppliance(propertyId: number, appliance: Appliance): Promise<void>`

To generate docs locally:
Creates an appliance record for a property.

```bash
npm run docs
```typescript
await client.createAppliance(123, {
item_name: 'Water Heater',
section_name: 'Basement',
brand: 'Rheem',
model: 'XE50M06ST45U1',
serial_number: 'ABC123',
manufacturer: 'Rheem Manufacturing',
year: '2020'
});
```

Then open `docs/index.html` in your browser.
### `Appliance`

| Field | Type | Description |
|-------|------|-------------|
| `item_name` | `string` | Name of the appliance |
| `section_name` | `string` | Location/section where found |
| `brand` | `string \| null` | Brand name |
| `model` | `string \| null` | Model number |
| `serial_number` | `string \| null` | Serial number |
| `manufacturer` | `string \| null` | Manufacturer name |
| `year` | `string \| null` | Year of manufacture |

## Development

Expand All @@ -127,14 +142,13 @@ npm run build
# Run tests
npm test

# Watch mode
npm run test:watch
# Run unit tests only
npm run test:unit

# Generate docs
npm run docs
# Run tests with coverage
npm run test:coverage
```

## License

MIT

13 changes: 10 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export interface InspectionRequest {
external_id: string;
inspection_date?: string;
inspector_name?: string;
inspector_image_url?: string;
notes?: string;
};
}
Expand Down Expand Up @@ -245,20 +246,26 @@ export class FixleClient {

/**
* Creates an inspection record for a property
*
*
* @param propertyId - ID of the property to associate the inspection with
* @param inspectionId - External inspection ID (from the source system)
* @param inspectorImageUrl - Optional URL to the inspector's profile image
* @returns Promise that resolves when the inspection is created
* @throws Error if the API request fails or the property doesn't exist
*
*
* @example
* await client.createInspection(123, 45678);
* console.log('Inspection created successfully');
*
* @example
* // With inspector image
* await client.createInspection(123, 45678, 'https://example.com/inspector.jpg');
*/
async createInspection(propertyId: number, inspectionId: number): Promise<void> {
async createInspection(propertyId: number, inspectionId: number, inspectorImageUrl?: string): Promise<void> {
const inspectionData: InspectionRequest = {
inspection: {
external_id: inspectionId.toString(),
...(inspectorImageUrl !== undefined && { inspector_image_url: inspectorImageUrl }),
},
};

Expand Down
87 changes: 64 additions & 23 deletions tests/client.test.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,91 @@
import { FixleClient } from '../src/client';

// Mock http and https modules
jest.mock('http');
jest.mock('https');
// Mock http module
jest.mock('http', () => ({
request: jest.fn(),
}));

describe('FixleClient', () => {
let client: FixleClient;
let mockRequest: jest.Mock;
let capturedBody: string;

beforeEach(() => {
jest.clearAllMocks();
capturedBody = '';
client = new FixleClient({
apiUrl: 'http://localhost:3000',
apiKey: 'test-api-key',
});

// Reset the mock before each test
const http = require('http');
mockRequest = http.request as jest.Mock;
mockRequest.mockReset();
});

const setupHttpMock = (responseBody: string, statusCode = 201) => {
const mockResponse = {
statusCode,
on: jest.fn((event: string, handler: (data?: string) => void) => {
if (event === 'data') handler(responseBody);
if (event === 'end') handler();
}),
};

mockRequest.mockImplementation((options: unknown, callback: (res: typeof mockResponse) => void) => {
callback(mockResponse);
return {
on: jest.fn(),
write: jest.fn((data: string) => { capturedBody = data; }),
end: jest.fn(),
};
});
};

describe('constructor', () => {
it('should create a client with config', () => {
expect(client).toBeInstanceOf(FixleClient);
});
});


describe('findOrCreateProperty', () => {
it('should parse address correctly', async () => {
const mockResponse = {
statusCode: 201,
on: jest.fn((event, handler) => {
if (event === 'data') handler('{"data":{"id":"123"}}');
if (event === 'end') handler();
}),
};

const http = require('http');
http.request = jest.fn((options, callback) => {
callback(mockResponse);
return {
on: jest.fn(),
write: jest.fn(),
end: jest.fn(),
};
});
setupHttpMock('{"data":{"id":"123"}}');

const propertyId = await client.findOrCreateProperty('123 Main St, Portland, OR 97201');
expect(propertyId).toBe(123);
});
});
});

describe('createInspection', () => {
it('should create inspection without inspectorImageUrl', async () => {
setupHttpMock('{"data":{"id":"456"}}');

await client.createInspection(123, 45678);

const parsedBody = JSON.parse(capturedBody);
expect(parsedBody.inspection.external_id).toBe('45678');
expect(parsedBody.inspection.inspector_image_url).toBeUndefined();
});

it('should create inspection with inspectorImageUrl', async () => {
setupHttpMock('{"data":{"id":"456"}}');

await client.createInspection(123, 45678, 'https://example.com/inspector.jpg');

const parsedBody = JSON.parse(capturedBody);
expect(parsedBody.inspection.external_id).toBe('45678');
expect(parsedBody.inspection.inspector_image_url).toBe('https://example.com/inspector.jpg');
});

it('should include empty string inspectorImageUrl when explicitly provided', async () => {
setupHttpMock('{"data":{"id":"456"}}');

await client.createInspection(123, 45678, '');

const parsedBody = JSON.parse(capturedBody);
expect(parsedBody.inspection.external_id).toBe('45678');
expect(parsedBody.inspection.inspector_image_url).toBe('');
});
});
});