convex-weflayr 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -63
- package/dist/client/api.d.ts +116 -0
- package/dist/client/api.d.ts.map +1 -0
- package/dist/client/api.js +130 -0
- package/dist/client/api.js.map +1 -0
- package/dist/client/index.d.ts +116 -8
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +184 -17
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/component.d.ts +42 -0
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/lib.d.ts +56 -14
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +97 -3
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.d.ts +39 -12
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +16 -0
- package/dist/component/schema.js.map +1 -1
- package/package.json +4 -3
- package/src/client/api.ts +230 -0
- package/src/client/index.ts +251 -19
- package/src/component/_generated/component.ts +54 -1
- package/src/component/lib.ts +108 -7
- package/src/component/schema.ts +17 -0
package/README.md
CHANGED
|
@@ -1,30 +1,42 @@
|
|
|
1
1
|
# convex-weflayr
|
|
2
2
|
|
|
3
|
-
A Convex component
|
|
4
|
-
|
|
5
|
-
customer
|
|
3
|
+
A Convex component for running your app on [Weflayr](https://weflayr.com)
|
|
4
|
+
routing strategies, with every call tagged by the customer it serves. It also
|
|
5
|
+
brings Weflayr's real cost and margin per customer back into your Convex
|
|
6
|
+
database, so your app can show it and act on it.
|
|
6
7
|
|
|
7
|
-
Weflayr
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
Weflayr is a gateway that decides which model answers each call. You benchmark
|
|
9
|
+
your own calls on other models, save the result as a routing strategy, and your
|
|
10
|
+
code sends `model: "strategy/<id>"`. Weflayr then picks the model per call, and
|
|
11
|
+
its routing overrides can treat a customer differently by their cost or their
|
|
12
|
+
margin. All of that depends on each call saying which customer, feature and call
|
|
13
|
+
it belongs to. This component does that in a Convex action, and keeps what comes
|
|
14
|
+
back where your queries can reach it.
|
|
14
15
|
|
|
15
|
-

|
|
16
17
|
|
|
17
18
|
## What you get
|
|
18
19
|
|
|
19
|
-
- **
|
|
20
|
-
|
|
21
|
-
call
|
|
22
|
-
- **
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- **
|
|
27
|
-
|
|
20
|
+
- **The customer on every call.** Read from your login or your own function.
|
|
21
|
+
Per-customer routing overrides, cost and margin in Weflayr all depend on it.
|
|
22
|
+
A call without a customer fails by default, so none go untagged.
|
|
23
|
+
- **Strategies instead of model names.** `strategy("support", 3)` pins a version
|
|
24
|
+
in production and `strategy("support")` follows the latest. Changing which
|
|
25
|
+
model answers is a change in the Weflayr dashboard, not a deploy. Each call is
|
|
26
|
+
saved with the model the strategy picked.
|
|
27
|
+
- **Revenue and plan into Weflayr.** Push revenue from any billing system and
|
|
28
|
+
tag customers with their plan, so margin and per-plan views work.
|
|
29
|
+
- **Weflayr's cost and margin in your database.** `sync` saves each customer's
|
|
30
|
+
real cost, revenue, margin and risk status for the month. Show it in your UI
|
|
31
|
+
with a live query, and stop a customer whose margin status you list.
|
|
32
|
+
- **Budgets.** A monthly dollar budget per customer, checked against that real
|
|
33
|
+
cost. The next call is stopped before the model runs, and other customers
|
|
34
|
+
carry on.
|
|
35
|
+
- **Agent runs.** Every step of a run carries one feature run id, so Weflayr
|
|
36
|
+
prices the whole run. You can also read the run's cost step by step and stop
|
|
37
|
+
it part way.
|
|
38
|
+
- **A per-call record.** Every call is saved with customer, call, feature, plan,
|
|
39
|
+
model, tokens, cost estimate, latency and status, for lists in your own UI.
|
|
28
40
|
- **Calls that survive a gateway failure.** `withFallback()` can run the call
|
|
29
41
|
direct if the gateway fails. This is optional.
|
|
30
42
|
- **A Vercel AI SDK model** for the gateway, if you use the AI SDK.
|
|
@@ -114,13 +126,18 @@ npx convex env set WEFLAYR_CUSTOMERS "*"
|
|
|
114
126
|
`WEFLAYR_CUSTOMERS` is a comma separated list of customer ids, or `*` for all.
|
|
115
127
|
When it is empty, no customer goes through the gateway.
|
|
116
128
|
|
|
129
|
+
Give the key the Gateway scope for model calls, and the Management scope as well
|
|
130
|
+
for cost, margin, revenue and tags. To use a separate management key, set
|
|
131
|
+
`WEFLAYR_MANAGEMENT_KEY`.
|
|
132
|
+
|
|
117
133
|
## Use it
|
|
118
134
|
|
|
119
|
-
Create one `Weflayr` instance. Name your calls
|
|
120
|
-
|
|
135
|
+
Create one `Weflayr` instance. Name your calls, the feature each belongs to, and
|
|
136
|
+
the model or strategy each uses. A benchmark in Weflayr is set up per call name,
|
|
137
|
+
so keep the names stable.
|
|
121
138
|
|
|
122
139
|
```ts
|
|
123
|
-
import { Weflayr } from "convex-weflayr";
|
|
140
|
+
import { Weflayr, strategy } from "convex-weflayr";
|
|
124
141
|
import { components } from "./_generated/api";
|
|
125
142
|
|
|
126
143
|
export const weflayr = new Weflayr(components.weflayr, {
|
|
@@ -129,37 +146,99 @@ export const weflayr = new Weflayr(components.weflayr, {
|
|
|
129
146
|
"classify-ticket": "support-replies",
|
|
130
147
|
"summarise-thread": "thread-summaries",
|
|
131
148
|
},
|
|
132
|
-
//
|
|
149
|
+
// Pin a version in production, follow the latest elsewhere.
|
|
150
|
+
models: {
|
|
151
|
+
"draft-reply": strategy("support", process.env.NODE_ENV === "production" ? 4 : undefined),
|
|
152
|
+
"classify-ticket": "claude-haiku-5-5",
|
|
153
|
+
},
|
|
154
|
+
defaultModel: strategy("default"),
|
|
155
|
+
// Who the call is for. The default is the signed-in user's id.
|
|
156
|
+
customer: async (ctx) => (await ctx.auth.getUserIdentity())?.orgId as string | undefined,
|
|
157
|
+
// Stop a customer whose margin is in one of these states.
|
|
158
|
+
stopOnMargin: ["unprofitable"],
|
|
159
|
+
// Dollars per million tokens, for estimates between syncs.
|
|
133
160
|
prices: {
|
|
134
161
|
"claude-haiku-5-5": { inputPerMillion: 0.1, outputPerMillion: 0.5 },
|
|
135
162
|
},
|
|
136
163
|
});
|
|
137
164
|
```
|
|
138
165
|
|
|
139
|
-
Call
|
|
166
|
+
Call the model through the gateway with the AI SDK. `callMeta` gives the
|
|
167
|
+
customer and the model for the call:
|
|
140
168
|
|
|
141
169
|
```ts
|
|
142
170
|
import { generateText } from "ai";
|
|
143
171
|
import { weflayrModel } from "convex-weflayr/ai-sdk";
|
|
144
172
|
|
|
145
|
-
const meta = {
|
|
173
|
+
const meta = await weflayr.callMeta(ctx, { call: "draft-reply", plan: workspace.plan });
|
|
146
174
|
|
|
147
|
-
const result = await weflayr.track(
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
headers: weflayr.headers(meta),
|
|
154
|
-
prompt,
|
|
155
|
-
}),
|
|
175
|
+
const result = await weflayr.track(ctx, meta, () =>
|
|
176
|
+
generateText({
|
|
177
|
+
model: weflayrModel(weflayr, meta.model),
|
|
178
|
+
headers: weflayr.headers(meta),
|
|
179
|
+
prompt,
|
|
180
|
+
}),
|
|
156
181
|
);
|
|
157
182
|
```
|
|
158
183
|
|
|
159
|
-
`ctx` is any action context. `track` times the call and saves it
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
184
|
+
`ctx` is any action context. `track` times the call and saves it, with the model
|
|
185
|
+
that answered when the model is a strategy. If the call fails, it saves the
|
|
186
|
+
failure and throws the error again. When the gateway changed the request, for
|
|
187
|
+
example by removing a setting the chosen model does not take, the warning is
|
|
188
|
+
logged and saved with the call. Without the AI SDK, `weflayr.connection()`
|
|
189
|
+
returns the base URL and key for any OpenAI compatible client.
|
|
190
|
+
|
|
191
|
+
A call with no customer throws, so nothing reaches Weflayr untagged. To log a
|
|
192
|
+
warning and send `unknown` instead, set `missingCustomer: "warn"`.
|
|
193
|
+
|
|
194
|
+
### Push revenue and plan
|
|
195
|
+
|
|
196
|
+
Weflayr computes margin from revenue. If you bill with Stripe, connect it in
|
|
197
|
+
Weflayr and skip this. For any other billing system, push revenue from your
|
|
198
|
+
billing webhook or a nightly job:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
await weflayr.sendRevenue([
|
|
202
|
+
{ customer_name: workspace.id, amount: 49, month: "2026-11", revenue_id: invoice.id },
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
await weflayr.tagCustomers({ [workspace.id]: { plan: "pro" } });
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Rows are sent as upserts keyed by `revenue_id`, so a retried webhook does not
|
|
209
|
+
book the same invoice twice, and a temporary Weflayr error is retried. Use one
|
|
210
|
+
revenue source per customer and month: Weflayr adds sources together.
|
|
211
|
+
|
|
212
|
+
### Bring cost and margin back
|
|
213
|
+
|
|
214
|
+
Run `sync` on a schedule. It reads each customer's cost, and margin when the
|
|
215
|
+
project has revenue, for the month and saves them in the component:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
// convex/crons.ts
|
|
219
|
+
import { cronJobs } from "convex/server";
|
|
220
|
+
import { internal } from "./_generated/api";
|
|
221
|
+
|
|
222
|
+
const crons = cronJobs();
|
|
223
|
+
crons.interval("sync weflayr", { minutes: 10 }, internal.billing.sync, {});
|
|
224
|
+
export default crons;
|
|
225
|
+
|
|
226
|
+
// convex/billing.ts
|
|
227
|
+
export const sync = internalAction({
|
|
228
|
+
args: {},
|
|
229
|
+
handler: async (ctx) => await weflayr.sync(ctx),
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Then read it from a query, so your UI updates live:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
const status = await weflayr.customerStatus(ctx, "acme");
|
|
237
|
+
// { costUsd, revenue, marginPct, riskStatus, syncedAt } or null before the first sync
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`riskStatus` is `healthy`, `watch`, `at_risk` or `unprofitable`. On a project
|
|
241
|
+
without revenue the cost is saved and the margin fields are empty.
|
|
163
242
|
|
|
164
243
|
### Streaming
|
|
165
244
|
|
|
@@ -170,13 +249,12 @@ result only has its usage when the stream ends, so record the call yourself in
|
|
|
170
249
|
```ts
|
|
171
250
|
const started = Date.now();
|
|
172
251
|
const result = streamText({
|
|
173
|
-
model: weflayrModel(weflayr,
|
|
252
|
+
model: weflayrModel(weflayr, meta.model),
|
|
174
253
|
headers: weflayr.headers(meta),
|
|
175
254
|
prompt,
|
|
176
255
|
onFinish: async ({ usage }) => {
|
|
177
256
|
await weflayr.record(ctx, {
|
|
178
257
|
...meta,
|
|
179
|
-
model: "claude-haiku-5-5",
|
|
180
258
|
inputTokens: usage.inputTokens ?? 0,
|
|
181
259
|
outputTokens: usage.outputTokens ?? 0,
|
|
182
260
|
latencyMs: Date.now() - started,
|
|
@@ -195,10 +273,10 @@ your own provider key.
|
|
|
195
273
|
```ts
|
|
196
274
|
const result = await weflayr.withFallback(
|
|
197
275
|
ctx,
|
|
198
|
-
|
|
276
|
+
meta,
|
|
199
277
|
{
|
|
200
|
-
gateway: () => generateText({ model: weflayrModel(weflayr, model), headers: weflayr.headers(meta), prompt }),
|
|
201
|
-
direct: () => generateText({ model: yourProvider(
|
|
278
|
+
gateway: () => generateText({ model: weflayrModel(weflayr, meta.model), headers: weflayr.headers(meta), prompt }),
|
|
279
|
+
direct: () => generateText({ model: yourProvider("claude-haiku-5-5"), prompt }),
|
|
202
280
|
},
|
|
203
281
|
);
|
|
204
282
|
```
|
|
@@ -210,9 +288,10 @@ saved.
|
|
|
210
288
|
### Group the calls of one job
|
|
211
289
|
|
|
212
290
|
Pass the same `runId` (`newRunId()` makes one) to every call of one job. It is
|
|
213
|
-
|
|
214
|
-
`weflayr.runCost(ctx, runId)` returns the cost of the whole
|
|
215
|
-
step.
|
|
291
|
+
sent to Weflayr as the feature run id, so Weflayr prices the whole job, and it is
|
|
292
|
+
saved with each call. `weflayr.runCost(ctx, runId)` returns the cost of the whole
|
|
293
|
+
job and of each step. A run id is 32 lowercase hex characters, and anything else
|
|
294
|
+
is rejected.
|
|
216
295
|
|
|
217
296
|
Weflayr also runs an MCP server at `https://mcp.weflayr.com/`, so an agent can
|
|
218
297
|
read cost and margin as a tool.
|
|
@@ -221,12 +300,16 @@ read cost and margin as a tool.
|
|
|
221
300
|
|
|
222
301
|
```ts
|
|
223
302
|
await weflayr.setBudget(ctx, "acme", 25); // 25 dollars a month, UTC calendar month
|
|
224
|
-
await weflayr.assertWithinBudget(ctx, "acme"); // throws
|
|
225
|
-
const status = await weflayr.budget(ctx, "acme"); // { limitUsd, spentUsd, remainingUsd, allowed }
|
|
303
|
+
await weflayr.assertWithinBudget(ctx, "acme"); // throws once spent, or on a listed margin status
|
|
304
|
+
const status = await weflayr.budget(ctx, "acme"); // { limitUsd, spentUsd, remainingUsd, allowed, source, riskStatus }
|
|
226
305
|
```
|
|
227
306
|
|
|
228
|
-
A customer without a budget always passes.
|
|
229
|
-
the month
|
|
307
|
+
A customer without a budget always passes. After a sync, spend is Weflayr's real
|
|
308
|
+
cost for the month plus the estimated cost of calls made since shortly before the
|
|
309
|
+
sync (`settleMs`, five minutes by default). In that window a call can be counted
|
|
310
|
+
twice, so spend reads slightly high, never low. Before the first sync, spend is
|
|
311
|
+
the sum of your saved estimates and `source` is `"local"`. A call already running
|
|
312
|
+
when the limit is reached can still finish.
|
|
230
313
|
|
|
231
314
|
### Read the numbers
|
|
232
315
|
|
|
@@ -246,16 +329,16 @@ updates live in your UI.
|
|
|
246
329
|
|
|
247
330
|
## How cost is worked out
|
|
248
331
|
|
|
249
|
-
|
|
250
|
-
|
|
332
|
+
Weflayr's dashboard is the source of truth for what you were billed, and `sync`
|
|
333
|
+
brings that figure into your database per customer and month. For each saved call
|
|
334
|
+
the gateway does not send a price back, so the component uses, in order:
|
|
251
335
|
|
|
252
336
|
1. A cost you pass: `costUsd` in the call details, or from `extract` in `track`.
|
|
253
337
|
Saved as `reported`.
|
|
254
338
|
2. An estimate from your `prices` table. Saved as `estimated`.
|
|
255
339
|
3. Zero, saved as `none`, when neither is available.
|
|
256
340
|
|
|
257
|
-
|
|
258
|
-
saved cost to compare customers and enforce budgets, and fill `prices` from your
|
|
341
|
+
Per-call costs feed your lists and the gap between syncs. Fill `prices` from your
|
|
259
342
|
provider's price list.
|
|
260
343
|
|
|
261
344
|
## What is saved
|
|
@@ -265,6 +348,7 @@ provider's price list.
|
|
|
265
348
|
| `calls` | model call | until you run `weflayr.purge(ctx, before)` |
|
|
266
349
|
| `daily` | customer, call and UTC day | always |
|
|
267
350
|
| `budgets` | customer | until cleared |
|
|
351
|
+
| `synced` | customer and month | replaced by each sync |
|
|
268
352
|
|
|
269
353
|
Queries read the `daily` rows, so they stay fast as `calls` grows. Days and
|
|
270
354
|
months are UTC.
|
|
@@ -273,17 +357,19 @@ months are UTC.
|
|
|
273
357
|
|
|
274
358
|
- Saving a call never fails the call. If the write fails, the component logs a
|
|
275
359
|
warning and returns your answer.
|
|
276
|
-
- The `plan
|
|
277
|
-
|
|
360
|
+
- The `plan` and any `tags` you pass are sent to Weflayr as tags on the request,
|
|
361
|
+
and `runId` as its feature run id.
|
|
278
362
|
- Customer names go to Weflayr as written. Use an id or a name you are happy to
|
|
279
363
|
see in its dashboard.
|
|
280
364
|
|
|
281
365
|
## Limitations
|
|
282
366
|
|
|
283
|
-
-
|
|
284
|
-
call.
|
|
285
|
-
- A budget compares
|
|
286
|
-
|
|
367
|
+
- Between syncs, cost is an estimate from your `prices` table unless you pass the
|
|
368
|
+
cost of a call.
|
|
369
|
+
- A budget compares spend with the limit before a call starts, so a call already
|
|
370
|
+
running can push a customer past it.
|
|
371
|
+
- Margin needs revenue in Weflayr, from Stripe, a CSV or `sendRevenue`. On an
|
|
372
|
+
internal project Weflayr has no margin, and `sync` saves cost only.
|
|
287
373
|
- A customer who is not on the allow list runs the direct call, and that call is
|
|
288
374
|
not saved.
|
|
289
375
|
- `usageByCustomer` and the other reports read one `daily` row per customer,
|
|
@@ -301,8 +387,15 @@ months are UTC.
|
|
|
301
387
|
**Cost shows $0.** The model is missing from `prices`. Add it, in dollars per
|
|
302
388
|
million tokens.
|
|
303
389
|
|
|
304
|
-
**A budget never stops anyone.**
|
|
305
|
-
|
|
390
|
+
**A budget never stops anyone.** Run `sync` on a schedule, or fill `prices`, so
|
|
391
|
+
there is a cost to compare.
|
|
392
|
+
|
|
393
|
+
**`No customer for the call`.** The call has no signed-in user and no `customer`
|
|
394
|
+
function returned one. Pass the customer, or set `missingCustomer: "warn"`.
|
|
395
|
+
|
|
396
|
+
**`unknown_routing_strategy` or `invalid_routing_strategy_version`.** The
|
|
397
|
+
strategy id or version does not exist in the project of your key. List them in
|
|
398
|
+
the Weflayr dashboard.
|
|
306
399
|
|
|
307
400
|
**The gateway returns 402.** Your Weflayr balance is empty. Add credit in the
|
|
308
401
|
Weflayr dashboard.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
export declare const MANAGEMENT_URL = "https://app.weflayr.com/api";
|
|
2
|
+
/** A customer's margin status in Weflayr. The thresholds behind each one are set in the Weflayr dashboard. */
|
|
3
|
+
export type RiskStatus = "healthy" | "watch" | "at_risk" | "unprofitable" | (string & {});
|
|
4
|
+
export type Aggregate = "day" | "week" | "month" | "quarter" | "year" | "all";
|
|
5
|
+
export type CostsInput = {
|
|
6
|
+
date_from: string;
|
|
7
|
+
date_to?: string;
|
|
8
|
+
aggregate?: Aggregate;
|
|
9
|
+
group_by?: "customer" | "feature" | "model" | "provider" | "call_name";
|
|
10
|
+
customers?: string[];
|
|
11
|
+
features?: string[];
|
|
12
|
+
call_names?: string[];
|
|
13
|
+
models?: string[];
|
|
14
|
+
providers?: string[];
|
|
15
|
+
customer_tags?: Record<string, string | string[]>;
|
|
16
|
+
run_tags?: Record<string, string | string[]>;
|
|
17
|
+
apply_credits?: boolean;
|
|
18
|
+
limit?: number;
|
|
19
|
+
cursor?: string;
|
|
20
|
+
};
|
|
21
|
+
export type CostRow = {
|
|
22
|
+
date_from: string;
|
|
23
|
+
date_to: string;
|
|
24
|
+
customer?: string;
|
|
25
|
+
feature?: string;
|
|
26
|
+
model?: string;
|
|
27
|
+
provider?: string;
|
|
28
|
+
call_name?: string;
|
|
29
|
+
cost: number;
|
|
30
|
+
avg_feature_run_cost?: number;
|
|
31
|
+
max_feature_run_cost?: number;
|
|
32
|
+
};
|
|
33
|
+
export type MarginInput = {
|
|
34
|
+
date_from: string;
|
|
35
|
+
date_to?: string;
|
|
36
|
+
aggregate?: Aggregate;
|
|
37
|
+
group_by?: "customer";
|
|
38
|
+
customers?: string[];
|
|
39
|
+
call_names?: string[];
|
|
40
|
+
filter_customers_based_on_features?: string[];
|
|
41
|
+
apply_credits?: boolean;
|
|
42
|
+
limit?: number;
|
|
43
|
+
cursor?: string;
|
|
44
|
+
};
|
|
45
|
+
export type MarginRow = {
|
|
46
|
+
date_from: string;
|
|
47
|
+
date_to: string;
|
|
48
|
+
customer?: string;
|
|
49
|
+
revenue: number;
|
|
50
|
+
cost: number;
|
|
51
|
+
margin: number;
|
|
52
|
+
margin_pct: number | null;
|
|
53
|
+
risk_status: RiskStatus;
|
|
54
|
+
};
|
|
55
|
+
export type Page<T> = {
|
|
56
|
+
rows: T[];
|
|
57
|
+
next_cursor: string | null;
|
|
58
|
+
has_more: boolean;
|
|
59
|
+
};
|
|
60
|
+
export type RevenueRow = {
|
|
61
|
+
/** The same customer name your model calls carry. */
|
|
62
|
+
customer_name: string;
|
|
63
|
+
amount: number;
|
|
64
|
+
/** "YYYY-MM". Or set month_start and month_end for a period longer than a month. */
|
|
65
|
+
month?: string;
|
|
66
|
+
month_start?: string;
|
|
67
|
+
month_end?: string;
|
|
68
|
+
/** An id from your billing system. With `upsert`, sending the same id again overwrites the row instead of adding to it. */
|
|
69
|
+
revenue_id?: string;
|
|
70
|
+
};
|
|
71
|
+
export type CustomerTags = {
|
|
72
|
+
customer_name: string;
|
|
73
|
+
/** A string sets the tag, null deletes it. Keys you leave out are not changed. */
|
|
74
|
+
tags: Record<string, string | null>;
|
|
75
|
+
};
|
|
76
|
+
export declare class WeflayrApiError extends Error {
|
|
77
|
+
readonly status: number;
|
|
78
|
+
constructor(status: number, message: string);
|
|
79
|
+
}
|
|
80
|
+
/** The first and last day of a "YYYY-MM" month. */
|
|
81
|
+
export declare function monthRange(month: string): {
|
|
82
|
+
from: string;
|
|
83
|
+
to: string;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* A small client for Weflayr's management API: cost, margin, revenue and customer tags.
|
|
87
|
+
* It needs a key with the Management scope.
|
|
88
|
+
*/
|
|
89
|
+
export declare class WeflayrApi {
|
|
90
|
+
private readonly options;
|
|
91
|
+
constructor(options?: {
|
|
92
|
+
apiKey?: string;
|
|
93
|
+
baseURL?: string;
|
|
94
|
+
});
|
|
95
|
+
private key;
|
|
96
|
+
private post;
|
|
97
|
+
getCosts(input: CostsInput): Promise<Page<CostRow>>;
|
|
98
|
+
getMargin(input: MarginInput): Promise<Page<MarginRow>>;
|
|
99
|
+
/** Books revenue per customer and month. Sends 1,000 rows per request. */
|
|
100
|
+
setRevenue(rows: RevenueRow[], options?: {
|
|
101
|
+
upsert?: boolean;
|
|
102
|
+
}): Promise<{
|
|
103
|
+
rowsInserted: number;
|
|
104
|
+
}>;
|
|
105
|
+
/** Sets tags such as the plan on customers. Sends 1,000 customers per request. */
|
|
106
|
+
setCustomerTags(customers: CustomerTags[], options?: {
|
|
107
|
+
upsert?: boolean;
|
|
108
|
+
}): Promise<{
|
|
109
|
+
customersUpdated: number;
|
|
110
|
+
}>;
|
|
111
|
+
/** What Weflayr has recorded for one customer in one month, in dollars, at list price. */
|
|
112
|
+
customerCost(customer: string, month: string): Promise<number>;
|
|
113
|
+
/** One customer's margin for one month, or null when Weflayr has no row for them. */
|
|
114
|
+
customerMargin(customer: string, month: string): Promise<MarginRow | null>;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/client/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
export const MANAGEMENT_URL = "https://app.weflayr.com/api";
|
|
2
|
+
export class WeflayrApiError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
constructor(status, message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.name = "WeflayrApiError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const MAX_ROWS = 1000;
|
|
11
|
+
function chunk(items) {
|
|
12
|
+
const out = [];
|
|
13
|
+
for (let i = 0; i < items.length; i += MAX_ROWS)
|
|
14
|
+
out.push(items.slice(i, i + MAX_ROWS));
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
/** The first and last day of a "YYYY-MM" month. */
|
|
18
|
+
export function monthRange(month) {
|
|
19
|
+
const [year, monthIndex] = month.split("-").map(Number);
|
|
20
|
+
const last = new Date(Date.UTC(year, monthIndex, 0)).getUTCDate();
|
|
21
|
+
return { from: `${month}-01`, to: `${month}-${String(last).padStart(2, "0")}` };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A small client for Weflayr's management API: cost, margin, revenue and customer tags.
|
|
25
|
+
* It needs a key with the Management scope.
|
|
26
|
+
*/
|
|
27
|
+
export class WeflayrApi {
|
|
28
|
+
options;
|
|
29
|
+
constructor(options = {}) {
|
|
30
|
+
this.options = options;
|
|
31
|
+
}
|
|
32
|
+
key() {
|
|
33
|
+
const key = this.options.apiKey ??
|
|
34
|
+
process.env.WEFLAYR_MANAGEMENT_KEY ??
|
|
35
|
+
process.env.WEFLAYR_API_KEY;
|
|
36
|
+
if (!key) {
|
|
37
|
+
throw new Error("Set WEFLAYR_MANAGEMENT_KEY, or WEFLAYR_API_KEY with the Management scope.");
|
|
38
|
+
}
|
|
39
|
+
return key;
|
|
40
|
+
}
|
|
41
|
+
async post(path, body) {
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
response = await fetch(`${this.options.baseURL ?? MANAGEMENT_URL}${path}`, {
|
|
45
|
+
method: "POST",
|
|
46
|
+
headers: {
|
|
47
|
+
authorization: `Bearer ${this.key()}`,
|
|
48
|
+
"content-type": "application/json",
|
|
49
|
+
},
|
|
50
|
+
body: JSON.stringify(body),
|
|
51
|
+
signal: AbortSignal.timeout(30_000),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof Error && error.message.startsWith("Set WEFLAYR_"))
|
|
56
|
+
throw error;
|
|
57
|
+
const cause = error instanceof Error && error.cause instanceof Error ? error.cause : error;
|
|
58
|
+
const code = cause.code;
|
|
59
|
+
throw new Error(`Could not reach Weflayr's management API (${this.options.baseURL ?? MANAGEMENT_URL}${path}): ${cause instanceof Error ? cause.message : String(cause)}${code ? ` [${code}]` : ""}`);
|
|
60
|
+
}
|
|
61
|
+
const text = await response.text();
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
let message = text.slice(0, 300);
|
|
64
|
+
try {
|
|
65
|
+
const parsed = JSON.parse(text);
|
|
66
|
+
message = String(parsed.error ?? JSON.stringify(parsed.detail ?? parsed)).slice(0, 300);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// keep the raw text
|
|
70
|
+
}
|
|
71
|
+
throw new WeflayrApiError(response.status, `Weflayr ${path} answered ${response.status}: ${message}`);
|
|
72
|
+
}
|
|
73
|
+
return JSON.parse(text);
|
|
74
|
+
}
|
|
75
|
+
getCosts(input) {
|
|
76
|
+
return this.post("/costs/", input);
|
|
77
|
+
}
|
|
78
|
+
getMargin(input) {
|
|
79
|
+
return this.post("/margin/", input);
|
|
80
|
+
}
|
|
81
|
+
/** Books revenue per customer and month. Sends 1,000 rows per request. */
|
|
82
|
+
async setRevenue(rows, options = {}) {
|
|
83
|
+
let inserted = 0;
|
|
84
|
+
for (const part of chunk(rows)) {
|
|
85
|
+
const result = await this.post("/revenue/", {
|
|
86
|
+
rows: part,
|
|
87
|
+
upsert: options.upsert ?? true,
|
|
88
|
+
});
|
|
89
|
+
inserted += result.rows_inserted ?? part.length;
|
|
90
|
+
}
|
|
91
|
+
return { rowsInserted: inserted };
|
|
92
|
+
}
|
|
93
|
+
/** Sets tags such as the plan on customers. Sends 1,000 customers per request. */
|
|
94
|
+
async setCustomerTags(customers, options = {}) {
|
|
95
|
+
let updated = 0;
|
|
96
|
+
for (const part of chunk(customers)) {
|
|
97
|
+
const result = await this.post("/customer-tags/", {
|
|
98
|
+
customers: part,
|
|
99
|
+
upsert: options.upsert ?? true,
|
|
100
|
+
});
|
|
101
|
+
updated += result.customers_updated ?? part.length;
|
|
102
|
+
}
|
|
103
|
+
return { customersUpdated: updated };
|
|
104
|
+
}
|
|
105
|
+
/** What Weflayr has recorded for one customer in one month, in dollars, at list price. */
|
|
106
|
+
async customerCost(customer, month) {
|
|
107
|
+
const { from, to } = monthRange(month);
|
|
108
|
+
const page = await this.getCosts({
|
|
109
|
+
date_from: from,
|
|
110
|
+
date_to: to,
|
|
111
|
+
aggregate: "all",
|
|
112
|
+
group_by: "customer",
|
|
113
|
+
customers: [customer],
|
|
114
|
+
});
|
|
115
|
+
return page.rows.reduce((sum, row) => sum + (row.cost ?? 0), 0);
|
|
116
|
+
}
|
|
117
|
+
/** One customer's margin for one month, or null when Weflayr has no row for them. */
|
|
118
|
+
async customerMargin(customer, month) {
|
|
119
|
+
const { from, to } = monthRange(month);
|
|
120
|
+
const page = await this.getMargin({
|
|
121
|
+
date_from: from,
|
|
122
|
+
date_to: to,
|
|
123
|
+
aggregate: "all",
|
|
124
|
+
group_by: "customer",
|
|
125
|
+
customers: [customer],
|
|
126
|
+
});
|
|
127
|
+
return page.rows[0] ?? null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/client/api.ts"],"names":[],"mappings":"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"}
|