Skip to content

Commit e5a245a

Browse files
rob-brownccclaude
andcommitted
Initial release: payload-plugin-unirate v0.1.0
Payload CMS v3 plugin for the UniRate currency-exchange API. Server-side proxy endpoints (rate/convert/currencies/vat) and a prefilled currency select field. Zero runtime deps. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
0 parents  commit e5a245a

18 files changed

Lines changed: 2650 additions & 0 deletions

.github/workflows/release.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
jobs:
9+
release:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
contents: write
13+
id-token: write
14+
environment:
15+
name: npm
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: actions/setup-node@v4
19+
with:
20+
node-version: "22"
21+
registry-url: "https://registry.npmjs.org"
22+
cache: npm
23+
- run: npm ci
24+
- run: npm test
25+
- run: npm run typecheck
26+
- run: npm run build
27+
- run: npm publish --provenance --access public
28+
env:
29+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
30+
- name: GitHub Release
31+
uses: softprops/action-gh-release@v2
32+
with:
33+
generate_release_notes: true

.github/workflows/test.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
node-version: ["20", "22"]
15+
steps:
16+
- uses: actions/checkout@v4
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: ${{ matrix.node-version }}
20+
cache: npm
21+
- run: npm ci
22+
- run: npm test
23+
- run: npm run typecheck
24+
- run: npm run build

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules/
2+
dist/
3+
*.tsbuildinfo
4+
.DS_Store
5+
coverage/
6+
*.log

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Unirate Team
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# payload-plugin-unirate
2+
3+
A [Payload CMS](https://payloadcms.com) **v3** plugin that integrates the
4+
[UniRate API](https://unirateapi.com) for currency exchange rates and VAT data.
5+
It appends four server-side endpoints under `/api/unirate` and ships an
6+
optional prefilled currency `select` field — your API key stays server-side,
7+
never exposed to clients. **Zero runtime dependencies** (`payload` is a peer).
8+
9+
## Install
10+
11+
```bash
12+
npm install payload-plugin-unirate
13+
```
14+
15+
Requires Payload `>=3.0.0` (peer dependency) and Node `>=18.20`.
16+
17+
## Quick start
18+
19+
```ts
20+
// payload.config.ts
21+
import { buildConfig } from "payload";
22+
import { uniratePlugin } from "payload-plugin-unirate";
23+
24+
export default buildConfig({
25+
// ...your usual config
26+
plugins: [
27+
uniratePlugin({
28+
apiKey: process.env.UNIRATE_API_KEY, // or omit and set the env var
29+
}),
30+
],
31+
});
32+
```
33+
34+
Set your key (recommended over inlining it):
35+
36+
```bash
37+
UNIRATE_API_KEY=your_key_here
38+
```
39+
40+
## Options
41+
42+
`uniratePlugin(options)`:
43+
44+
| Option | Type | Default | Description |
45+
|--------|------|---------|-------------|
46+
| `apiKey` | `string` | `process.env.UNIRATE_API_KEY` | UniRate API key (stays server-side). |
47+
| `baseUrl` | `string` | `https://api.unirateapi.com` | API base URL. |
48+
| `disabled` | `boolean` | `false` | When `true`, the plugin is a no-op and adds no endpoints. |
49+
| `timeoutMs` | `number` | `30000` | Per-request timeout. |
50+
51+
## Endpoints
52+
53+
The plugin appends these custom endpoints (Payload serves custom endpoints
54+
under the `/api` prefix):
55+
56+
| Method | Path | Description |
57+
|--------|------|-------------|
58+
| GET | `/api/unirate/rate?from=USD&to=EUR` | Exchange rate (single, or all rates for a base when `to` is omitted) |
59+
| GET | `/api/unirate/convert?from=USD&to=EUR&amount=100` | Convert an amount |
60+
| GET | `/api/unirate/currencies` | List supported currencies |
61+
| GET | `/api/unirate/vat?country=DE` | VAT rates (optional country filter) |
62+
63+
Example:
64+
65+
```bash
66+
curl "http://localhost:3000/api/unirate/convert?from=USD&to=EUR&amount=100"
67+
# → { "from": "USD", "to": "EUR", "amount": 100, "result": 92.00 }
68+
```
69+
70+
When no API key is configured, the endpoints respond `503` (and log a warning)
71+
rather than crashing at boot.
72+
73+
## Currency select field
74+
75+
An exported helper builds a Payload `select` field prefilled with common
76+
ISO-4217 currency codes:
77+
78+
```ts
79+
import { currencyField } from "payload-plugin-unirate";
80+
81+
const Products = {
82+
slug: "products",
83+
fields: [
84+
{ name: "title", type: "text" },
85+
currencyField({ name: "priceCurrency", defaultValue: "USD", required: true }),
86+
],
87+
};
88+
```
89+
90+
Pass `options: ["USD", "EUR", ...]` to override the code list, or fetch the live
91+
list from `/api/unirate/currencies`.
92+
93+
## Error handling
94+
95+
Endpoint responses map upstream UniRate errors to the appropriate HTTP status:
96+
97+
| Status | Meaning |
98+
|--------|---------|
99+
| 400 | Invalid request parameters |
100+
| 401 | Missing or invalid API key |
101+
| 403 | Endpoint requires a Pro subscription |
102+
| 404 | Currency not found / no data |
103+
| 429 | Rate limit exceeded |
104+
| 503 | Service unavailable / plugin not configured |
105+
| 502 | Upstream/transport failure |
106+
107+
The internal client and its typed error classes are also exported for direct
108+
use in hooks or custom endpoints:
109+
110+
```ts
111+
import { UniRateClient, AuthenticationError, RateLimitError, ProRequiredError } from "payload-plugin-unirate";
112+
113+
const client = new UniRateClient({ apiKey: process.env.UNIRATE_API_KEY! });
114+
try {
115+
const rate = await client.getRate("USD", "EUR"); // → 0.92
116+
} catch (err) {
117+
if (err instanceof AuthenticationError) { /* invalid key */ }
118+
if (err instanceof RateLimitError) { /* slow down */ }
119+
if (err instanceof ProRequiredError) { /* upgrade plan */ }
120+
}
121+
```
122+
123+
## Free vs Pro tier
124+
125+
Free-tier endpoints: rates, convert, currencies, VAT rates. Historical data and
126+
time series require a [Pro subscription](https://unirateapi.com/pricing).
127+
128+
## Related packages
129+
130+
<!-- unirate-ecosystem-start -->
131+
**UniRate API client libraries:** [Python](https://github.com/UniRate-API/unirate-api-python) · [Node.js](https://github.com/UniRate-API/unirate-api-nodejs) · [Go](https://github.com/UniRate-API/unirate-api-go) · [Rust](https://github.com/UniRate-API/unirate-api-rust) · [Ruby](https://github.com/UniRate-API/unirate-api-ruby) · [PHP](https://github.com/UniRate-API/unirate-api-php) · [Java](https://github.com/UniRate-API/unirate-api-java) · [Swift](https://github.com/UniRate-API/unirate-api-swift) · [.NET](https://github.com/UniRate-API/unirate-api-dotnet)
132+
133+
**Framework integrations:** [Next.js](https://github.com/UniRate-API/next-unirate) · [Nuxt](https://github.com/UniRate-API/nuxt-unirate) · [SvelteKit](https://github.com/UniRate-API/sveltekit-unirate) · [Astro](https://github.com/UniRate-API/astro-unirate) · [NestJS](https://github.com/UniRate-API/nestjs-unirate) · [Eleventy](https://github.com/UniRate-API/eleventy-unirate) · [React](https://github.com/UniRate-API/react-unirate) · [Vue](https://github.com/UniRate-API/vue-unirate) · [tRPC](https://github.com/UniRate-API/trpc-unirate)
134+
135+
**CMS & e-commerce:** [WordPress](https://github.com/UniRate-API/unirate-currency-converter) · [Directus](https://github.com/UniRate-API/directus-extension-unirate) · [Strapi](https://github.com/UniRate-API/strapi-plugin-unirate) · **Payload** (this package) · [Medusa](https://github.com/UniRate-API/medusa-plugin-unirate) · [Hugo](https://github.com/UniRate-API/hugo-unirate) · [Jekyll](https://github.com/UniRate-API/jekyll-unirate)
136+
137+
**Data & AI:** [LangChain Python](https://github.com/UniRate-API/langchain-unirate) · [LangChain.js](https://github.com/UniRate-API/langchain-js-unirate) · [FastAPI](https://github.com/UniRate-API/fastapi-unirate) · [Flask](https://github.com/UniRate-API/flask-unirate) · [Django REST](https://github.com/UniRate-API/djangorestframework-unirate) · [dbt](https://github.com/UniRate-API/dbt-unirate) · [Airflow](https://github.com/UniRate-API/airflow-provider-unirate)
138+
139+
**Other:** [MCP server](https://github.com/UniRate-API/unirate-mcp) · [CLI](https://github.com/UniRate-API/unirate-cli) · [Obsidian](https://github.com/UniRate-API/obsidian-currency) · [money gem](https://github.com/UniRate-API/money-unirate-api) · [laravel-money](https://github.com/UniRate-API/laravel-money-unirate)
140+
<!-- unirate-ecosystem-end -->
141+
142+
## License
143+
144+
MIT © Unirate Team

examples/payload.config.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Example: registering payload-plugin-unirate in a Payload CMS v3 config.
3+
*
4+
* The plugin appends four server-side endpoints under `/api/unirate`:
5+
* GET /api/unirate/rate?from=USD&to=EUR
6+
* GET /api/unirate/convert?from=USD&to=EUR&amount=100
7+
* GET /api/unirate/currencies
8+
* GET /api/unirate/vat?country=DE
9+
*
10+
* Your UniRate API key stays server-side — it is never sent to the browser.
11+
*
12+
* Run your app, then:
13+
* curl "http://localhost:3000/api/unirate/convert?from=USD&to=EUR&amount=100"
14+
*/
15+
16+
import { buildConfig } from "payload";
17+
import { uniratePlugin, currencyField } from "payload-plugin-unirate";
18+
19+
export default buildConfig({
20+
// ...your db adapter, secret, admin config, etc.
21+
collections: [
22+
{
23+
slug: "products",
24+
fields: [
25+
{ name: "title", type: "text" },
26+
{ name: "price", type: "number" },
27+
// A prefilled currency <select> using the exported helper:
28+
currencyField({ name: "priceCurrency", defaultValue: "USD", required: true }),
29+
],
30+
},
31+
],
32+
plugins: [
33+
uniratePlugin({
34+
// Reads process.env.UNIRATE_API_KEY when `apiKey` is omitted.
35+
apiKey: process.env.UNIRATE_API_KEY,
36+
// baseUrl: "https://api.unirateapi.com", // default
37+
// disabled: process.env.NODE_ENV === "test", // optionally turn the plugin off
38+
}),
39+
],
40+
});

0 commit comments

Comments
 (0)