-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 6d71b2d
Showing
18 changed files
with
9,951 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
PICA_API_KEY= | ||
PICA_CONNECTION_KEY= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
name: Publish Package to NPM | ||
|
||
on: | ||
release: | ||
types: [created] | ||
|
||
jobs: | ||
build: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
- uses: actions/setup-node@v4 | ||
with: | ||
node-version: '20.x' | ||
registry-url: 'https://registry.npmjs.org/' | ||
- name: Install dependencies | ||
run: | | ||
if [ -f package-lock.json ]; then | ||
npm ci | ||
else | ||
npm install | ||
fi | ||
- name: Run tests | ||
run: npm test | ||
- name: Build | ||
run: npm run build | ||
- name: Publish | ||
run: npm publish | ||
env: | ||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
node_modules/ | ||
.env | ||
.DS_Store | ||
dist |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# @picahq/unified | ||
|
||
The Pica Unified SDK library for Node.js provides convenient access to the Pica UnifiedAPI from applications written in server-side JavaScript. | ||
|
||
## Install | ||
|
||
Using npm: | ||
|
||
```jsx | ||
npm i @picahq/unified | ||
``` | ||
|
||
Using yarn: | ||
|
||
```jsx | ||
yarn add @picahq/unified | ||
``` | ||
|
||
## Configuration | ||
|
||
To use the library you must provide an API key and Connection key. Both are located in the Pica dashboard. | ||
|
||
```jsx | ||
import { Pica } from "@picahq/unified"; | ||
|
||
const pica = new Pica("sk_live_1234"); | ||
|
||
const response = await pica | ||
.customers("live::xero::acme-inc") | ||
.get("cus_OT3CLnirqcpjvw"); | ||
|
||
console.log(response); | ||
``` | ||
|
||
## Testing | ||
|
||
Configure the `.env` file based on the `.env.sample` provided with an Pica Secret Key, Connection Key and Model to test. | ||
|
||
## Full Documentation | ||
|
||
Please refer to the official Pica [Documentation](https://docs.picaos.com) and [API Reference](https://docs.picaos.com/reference) for more information and Node.js usage examples. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import fs from 'fs/promises'; | ||
import path from 'path'; | ||
import axios from 'axios'; | ||
import Handlebars from 'handlebars'; | ||
import { camelCase } from "camel-case"; | ||
|
||
const GET_TYPES_URL = "https://development-api.picaos.com/v1/public/schemas/types/typescript"; | ||
const GET_COMMON_MODELS_URL = "https://development-api.picaos.com/v1/public/sdk/common-models?limit=500"; | ||
|
||
const generateSDK = async () => { | ||
try { | ||
console.log('🚀 Fetching types from Pica API...'); | ||
|
||
const getTypesRequest = await axios.get(GET_TYPES_URL); | ||
const types = getTypesRequest.data; | ||
|
||
console.log('✅ Types fetched successfully. Updating models.ts file...'); | ||
|
||
const modelsFilePath = path.join(__dirname, '..', 'src', 'types', 'models.ts'); | ||
const modelsFileContent = `// This file is auto-generated. Do not edit it manually.\n\n${types}`; | ||
|
||
await fs.writeFile(modelsFilePath, modelsFileContent, 'utf8'); | ||
|
||
console.log('📝 models.ts file has been updated successfully.'); | ||
|
||
console.log('🔍 Fetching resources from Pica API...'); | ||
|
||
const getCommonModelsRequest = await axios.get(GET_COMMON_MODELS_URL); | ||
const commonModels = getCommonModelsRequest.data?.rows || []; | ||
const primaryCommonModelNames = commonModels | ||
.filter((model: any) => model.primary) | ||
.map((model: any) => model.name); | ||
|
||
const resources = primaryCommonModelNames.map((resourceName: string) => ({ | ||
lowerCase: resourceName.toLowerCase(), | ||
camelCase: camelCase(resourceName), | ||
pascalCase: resourceName | ||
})); | ||
|
||
console.log('✅ Primary common models fetched successfully. Generating index.ts file...'); | ||
|
||
const templatePath = path.join(__dirname, './templates/index.handlebars'); | ||
const templateContent = await fs.readFile(templatePath, 'utf8'); | ||
|
||
const template = Handlebars.compile(templateContent); | ||
const indexContent = template({ resources }); | ||
|
||
const indexFilePath = path.join(__dirname, '..', 'src', 'index.ts'); | ||
await fs.writeFile(indexFilePath, indexContent, 'utf8'); | ||
|
||
console.log('🎉 index.ts file has been generated successfully.'); | ||
} catch (error) { | ||
console.error('❌ An error occurred while generating the SDK:', error); | ||
} | ||
}; | ||
|
||
generateSDK(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
import axios, { AxiosInstance, Method } from 'axios'; | ||
import { UnifiedOptions, Response, ListFilter, ListResponse, Count, DeleteOptions, HttpStatusCode, ResponseError } from './types/'; | ||
import { convertFilterToQueryParams } from './utils'; | ||
|
||
export * from './types/generic'; | ||
export * from './types/models'; | ||
export * from './paginate'; | ||
|
||
import { | ||
{{#each resources}} | ||
{{this.pascalCase}}{{#unless @last}}, {{/unless}} | ||
{{/each}} | ||
} from './types/'; | ||
|
||
export interface UnifiedApi<Type> { | ||
create(object: Type, options?: UnifiedOptions | undefined | null): Promise<Response<Type>>; | ||
list(filter?: ListFilter | undefined | null, options?: UnifiedOptions | undefined | null): Promise<ListResponse<Type>>; | ||
get(id: string, options?: UnifiedOptions | undefined | null): Promise<Response<Type>>; | ||
update(id: string, object: Type, options?: UnifiedOptions | undefined | null): Promise<Response<Type>>; | ||
count(options?: UnifiedOptions | undefined | null): Promise<Response<Count>>; | ||
delete(id: string, deleteOptions?: DeleteOptions | undefined | null, options?: UnifiedOptions | undefined | null): Promise<Response<Type>>; | ||
} | ||
|
||
interface PassthroughAPI<Type> { | ||
call(options: { | ||
method: Method; | ||
path: string; | ||
data?: any; | ||
headers?: Record<string, string>; | ||
queryParams?: Record<string, string>; | ||
}): Promise<Response<Type>>; | ||
} | ||
|
||
export class Resource { | ||
protected axiosInstance: AxiosInstance; | ||
protected connectionKey: string; | ||
protected resourceName: string; | ||
|
||
constructor(axiosInstance: AxiosInstance, connectionKey: string, resourceName: string) { | ||
this.axiosInstance = axiosInstance; | ||
this.connectionKey = connectionKey; | ||
this.resourceName = resourceName; | ||
} | ||
|
||
protected getRequestHeaders(options?: UnifiedOptions): Record<string, string> { | ||
const headers: Record<string, string> = {}; | ||
const excludedKeys = ['common', 'delete', 'get', 'head', 'post', 'put', 'patch']; | ||
|
||
for (const [key, value] of Object.entries(this.axiosInstance.defaults.headers)) { | ||
if (!excludedKeys.includes(key) && typeof value === 'string') { | ||
headers[key] = value; | ||
} | ||
} | ||
|
||
headers['x-pica-connection-key'] = this.connectionKey; | ||
Object.assign(headers, options?.passthroughHeaders); | ||
|
||
return headers; | ||
} | ||
|
||
protected async makeRequestSingle<R>( | ||
method: string, | ||
url: string, | ||
data?: any, | ||
options?: UnifiedOptions, | ||
queryParams?: Record<string, string>, | ||
statusCode?: number | ||
): Promise<Response<R>> { | ||
try { | ||
const response = await this.axiosInstance.request({ | ||
method: method as Method, | ||
url, | ||
data, | ||
headers: this.getRequestHeaders(options), | ||
params: { ...queryParams, ...options?.passthroughQuery } | ||
}); | ||
|
||
const output = { | ||
...(url.startsWith('/passthrough') ? { passthrough: response?.data } : response?.data), | ||
headers: response.headers as Record<string, string>, | ||
statusCode: statusCode || response.status | ||
}; | ||
|
||
return output; | ||
} catch (error: any) { | ||
throw error.response?.data as ResponseError; | ||
} | ||
} | ||
|
||
protected async makeRequestList<R>( | ||
method: string, | ||
url: string, | ||
data?: any, | ||
options?: UnifiedOptions, | ||
queryParams?: Record<string, string>, | ||
statusCode?: number | ||
): Promise<ListResponse<R>> { | ||
try { | ||
const response = await this.axiosInstance.request({ | ||
method: method as Method, | ||
url, | ||
data, | ||
headers: this.getRequestHeaders(options), | ||
params: { ...queryParams, ...options?.passthroughQuery } | ||
}); | ||
|
||
const output = { | ||
...response?.data, | ||
headers: response.headers as Record<string, string>, | ||
statusCode: statusCode || response.status | ||
}; | ||
|
||
return output; | ||
} catch (error: any) { | ||
throw error.response?.data as ResponseError; | ||
} | ||
} | ||
} | ||
|
||
export class UnifiedResourceImpl<T> extends Resource implements UnifiedApi<T> { | ||
async create(object: T, options?: UnifiedOptions): Promise<Response<T>> { | ||
return this.makeRequestSingle<T>('POST', `/unified/${this.resourceName}`, object, options, undefined, HttpStatusCode.Created); | ||
} | ||
|
||
async upsert(object: T, options?: UnifiedOptions): Promise<Response<T>> { | ||
return this.makeRequestSingle<T>('PUT', `/unified/${this.resourceName}`, object, options, undefined, HttpStatusCode.OK); | ||
} | ||
|
||
async list(filter?: ListFilter, options?: UnifiedOptions): Promise<ListResponse<T>> { | ||
const queryParams = convertFilterToQueryParams(filter); | ||
return this.makeRequestList<T>('GET', `/unified/${this.resourceName}`, undefined, options, queryParams, HttpStatusCode.OK); | ||
} | ||
|
||
async get(id: string, options?: UnifiedOptions): Promise<Response<T>> { | ||
return this.makeRequestSingle<T>('GET', `/unified/${this.resourceName}/${id}`, undefined, options, undefined, HttpStatusCode.OK); | ||
} | ||
|
||
async update(id: string, object: T, options?: UnifiedOptions): Promise<Response<T>> { | ||
return this.makeRequestSingle<T>('PATCH', `/unified/${this.resourceName}/${id}`, object, options, undefined, HttpStatusCode.NoContent); | ||
} | ||
|
||
async count(options?: UnifiedOptions): Promise<Response<Count>> { | ||
return this.makeRequestSingle<Count>('GET', `/unified/${this.resourceName}/count`, undefined, options, undefined, HttpStatusCode.OK); | ||
} | ||
|
||
async delete(id: string, deleteOptions?: DeleteOptions, options?: UnifiedOptions): Promise<Response<T>> { | ||
return this.makeRequestSingle<T>('DELETE', `/unified/${this.resourceName}/${id}`, undefined, options, { | ||
...deleteOptions, | ||
}, HttpStatusCode.NoContent); | ||
} | ||
} | ||
|
||
class PassthroughResourceImpl<T> extends Resource implements PassthroughAPI<T> { | ||
async call<T>(options: { | ||
method: Method; | ||
path: string; | ||
data?: any; | ||
headers?: Record<string, string>; | ||
queryParams?: Record<string, string>; | ||
}): Promise<Response<T>> { | ||
const { method, path, data, headers, queryParams } = options; | ||
|
||
return this.makeRequestSingle<T>(method, `/passthrough/${path}`, data, headers, queryParams); | ||
} | ||
} | ||
|
||
export interface PicaConfig { | ||
serverUrl: string | ||
} | ||
|
||
export class Pica { | ||
public axiosInstance: AxiosInstance; | ||
|
||
constructor(private apiKey: string, options?: PicaConfig | undefined | null) { | ||
this.axiosInstance = axios.create({ | ||
baseURL: options?.serverUrl || 'https://api.picaos.com/v1', | ||
headers: { | ||
'x-pica-secret': this.apiKey, | ||
'Content-Type': 'application/json', | ||
}, | ||
}); | ||
} | ||
|
||
passthrough<T>(connectionKey: string): PassthroughAPI<T> { | ||
return new PassthroughResourceImpl(this.axiosInstance, connectionKey, 'passthrough'); | ||
} | ||
|
||
{{#each resources}} | ||
{{this.camelCase}}(connectionKey: string) { | ||
return new UnifiedResourceImpl<{{this.pascalCase}}>(this.axiosInstance, connectionKey, '{{this.lowerCase}}'); | ||
} | ||
{{/each}} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
module.exports = { | ||
preset: 'ts-jest', | ||
testEnvironment: 'node', | ||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], | ||
testMatch: ['**/tests/**/*.spec.ts'], | ||
transform: { | ||
'^.+\\.tsx?$': 'ts-jest', | ||
}, | ||
}; |
Oops, something went wrong.