@butinapp/sdk 0.1.0
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/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/data/builders.d.ts +111 -0
- package/dist/data/builders.js +103 -0
- package/dist/data/currency.d.ts +3 -0
- package/dist/data/currency.js +11 -0
- package/dist/data/dataset.d.ts +229 -0
- package/dist/data/dataset.js +70 -0
- package/dist/data/index.d.ts +13 -0
- package/dist/data/index.js +11 -0
- package/dist/data/result.d.ts +163 -0
- package/dist/data/result.js +171 -0
- package/dist/data/roles.d.ts +17 -0
- package/dist/data/roles.js +3 -0
- package/dist/data/series.d.ts +6 -0
- package/dist/data/series.js +5 -0
- package/dist/data/summary.d.ts +36 -0
- package/dist/data/summary.js +18 -0
- package/dist/data/view.d.ts +93 -0
- package/dist/data/view.js +74 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +14 -0
- package/dist/integrations/index.d.ts +1 -0
- package/dist/integrations/index.js +4 -0
- package/dist/integrations/stripe.d.ts +45 -0
- package/dist/integrations/stripe.js +102 -0
- package/dist/libs.d.ts +2 -0
- package/dist/libs.js +6 -0
- package/dist/plugin/auth.d.ts +49 -0
- package/dist/plugin/auth.js +1 -0
- package/dist/plugin/browser.d.ts +26 -0
- package/dist/plugin/browser.js +8 -0
- package/dist/plugin/capability.d.ts +54 -0
- package/dist/plugin/capability.js +19 -0
- package/dist/plugin/config.d.ts +61 -0
- package/dist/plugin/config.js +22 -0
- package/dist/plugin/documents.d.ts +5 -0
- package/dist/plugin/documents.js +1 -0
- package/dist/plugin/meta.d.ts +21 -0
- package/dist/plugin/meta.js +1 -0
- package/dist/plugin/plugin.d.ts +26 -0
- package/dist/plugin/plugin.js +1 -0
- package/dist/plugin/session.d.ts +33 -0
- package/dist/plugin/session.js +5 -0
- package/dist/plugin/transport.d.ts +42 -0
- package/dist/plugin/transport.js +1 -0
- package/dist/presets/apikeys.d.ts +13 -0
- package/dist/presets/apikeys.js +24 -0
- package/dist/presets/billing.d.ts +56 -0
- package/dist/presets/billing.js +209 -0
- package/dist/presets/blocks.d.ts +43 -0
- package/dist/presets/blocks.js +99 -0
- package/dist/presets/index.d.ts +38 -0
- package/dist/presets/index.js +22 -0
- package/dist/presets/members.d.ts +11 -0
- package/dist/presets/members.js +15 -0
- package/dist/presets/mtd-basis.d.ts +11 -0
- package/dist/presets/mtd-basis.js +16 -0
- package/dist/presets/usage.d.ts +17 -0
- package/dist/presets/usage.js +61 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +6 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +6 -0
- package/dist/testing/synthetic.d.ts +65 -0
- package/dist/testing/synthetic.js +259 -0
- package/dist/util/date.d.ts +14 -0
- package/dist/util/date.js +49 -0
- package/dist/util/fx.d.ts +2 -0
- package/dist/util/fx.js +7 -0
- package/dist/util/index.d.ts +6 -0
- package/dist/util/index.js +9 -0
- package/dist/util/money.d.ts +6 -0
- package/dist/util/money.js +35 -0
- package/dist/util/object.d.ts +2 -0
- package/dist/util/object.js +7 -0
- package/dist/util/text.d.ts +2 -0
- package/dist/util/text.js +16 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yann Allard
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# `@butinapp/sdk`
|
|
2
|
+
|
|
3
|
+
The plugin contract for [Butin](../../README.md). A plugin connects one service: you sign in once so Butin
|
|
4
|
+
captures your session, then it fetches from that service headless and brings the data home, normalized. This
|
|
5
|
+
package is **pure data + types** — no Electron, no React. Core runs your descriptor; one generic renderer in
|
|
6
|
+
`@butinapp/ui` draws whatever a capability returns.
|
|
7
|
+
|
|
8
|
+
## A plugin in one object
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import { definePlugin } from '@butinapp/sdk'
|
|
12
|
+
import { billing } from '@butinapp/sdk/presets'
|
|
13
|
+
|
|
14
|
+
export const acmePlugin = definePlugin({
|
|
15
|
+
meta: { id: 'acme', name: 'Acme', color: '#3b82f6' },
|
|
16
|
+
session: { loginUrl: 'https://acme.com/login', dashboardMarkers: ['/dashboard'], cookieDomains: ['acme.com'] },
|
|
17
|
+
auth: { kind: 'cookie' }, // replay the stored cookie verbatim
|
|
18
|
+
capabilities: [
|
|
19
|
+
{ id: 'billing', label: 'Billing', collect: async (ctx) => billing.result(await fetchAcmeBilling(ctx)) }
|
|
20
|
+
]
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`collect(ctx)` gets an authed `ctx.client` (auth + transport pre-applied) and returns a **`CapabilityResult`**
|
|
25
|
+
(`{ datasets, views?, summary? }`). Prefer the **presets** on `@butinapp/sdk/presets` (`billing.result` ·
|
|
26
|
+
`billing.summary` · `usage.result` · `keys.result` · `members.result`) over hand-building datasets/views —
|
|
27
|
+
they map normalized input onto the renderer for you.
|
|
28
|
+
|
|
29
|
+
## Every service is three declarative axes
|
|
30
|
+
|
|
31
|
+
| Axis | Where | Options |
|
|
32
|
+
| ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
33
|
+
| **Transport** | `transport.engine` | `node` (axios) · `electron` (real browser identity, for sites that need the browser engine — set `requiresBrowserEngine: true`) |
|
|
34
|
+
| **Auth** | `auth.kind` | `cookie` · `bearer-token` · `external` · `api-key` · `cookie-csrf` · `minted-jwt` · `rotating-refresh` · `spa-bearer` |
|
|
35
|
+
| **Render shape** | inside `collect()` | JSON · GraphQL · tRPC · Remix/RSC · HTML scrape · gRPC-web — parse however the service needs |
|
|
36
|
+
|
|
37
|
+
The source files are the reference, each documented inline: `meta.ts` · `session.ts` · `auth.ts` ·
|
|
38
|
+
`transport.ts` · `config.ts` · `capability.ts` · the data-view contract (`dataset.ts` · `view.ts` ·
|
|
39
|
+
`summary.ts` · `result.ts` · `presets/*`).
|
|
40
|
+
|
|
41
|
+
## Typed settings
|
|
42
|
+
|
|
43
|
+
Declare a plugin's settings once with `defineConfigSchema`; `definePlugin` infers `ctx.config`'s type from it — no
|
|
44
|
+
explicit generic, no annotation:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const acmeConfig = defineConfigSchema([{ key: 'orgId', label: 'Organization', kind: 'text', required: true }])
|
|
48
|
+
type AcmeConfig = ConfigOf<typeof acmeConfig> // { orgId: string }
|
|
49
|
+
|
|
50
|
+
export const acmePlugin = definePlugin({ config: acmeConfig /* ctx.config is AcmeConfig in every collect() */ })
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Authoring a plugin
|
|
54
|
+
|
|
55
|
+
1. Scaffold: `pnpm new-plugin <id>` (no `pnpm install` — a plugin is a folder in the existing
|
|
56
|
+
`@butinapp/plugins` package, auto-discovered, no registration).
|
|
57
|
+
2. Put everything in `src/main.ts`: the `definePlugin`, the `collect()` bodies, and the pure `build*()`
|
|
58
|
+
transforms (raw payload → `CapabilityResult`).
|
|
59
|
+
3. Test the **pure `build*()`** against a redacted fixture and assert with `validateCapabilityResult` — that's
|
|
60
|
+
where coverage lives. See `plugins/serper/` for the canonical example.
|
|
61
|
+
|
|
62
|
+
The repo root `CONTRIBUTING.md` and `packages/website` (user docs) have the full guide. `@butinapp/*` packages are
|
|
63
|
+
bundled into the app, not published to npm.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type Accrual, type BadgeTone, type Column, type Dataset, type RecordDataset, type ResetPeriod, type TableDataset } from './dataset.js';
|
|
2
|
+
import type { CapabilityResult } from './result.js';
|
|
3
|
+
import type { RolesFor } from './roles.js';
|
|
4
|
+
import type { Section, Summary } from './summary.js';
|
|
5
|
+
import type { View } from './view.js';
|
|
6
|
+
export type ViewSpec = {
|
|
7
|
+
view: View;
|
|
8
|
+
dataset: Dataset;
|
|
9
|
+
extraDatasets?: Dataset[];
|
|
10
|
+
};
|
|
11
|
+
type TypedColumn<Row, K extends keyof Row> = {
|
|
12
|
+
key: K;
|
|
13
|
+
role: RolesFor<Exclude<Row[K], undefined>>;
|
|
14
|
+
label?: string;
|
|
15
|
+
currency?: string;
|
|
16
|
+
badges?: Record<string, BadgeTone>;
|
|
17
|
+
accrual?: Accrual;
|
|
18
|
+
resetPeriod?: ResetPeriod;
|
|
19
|
+
truncate?: boolean;
|
|
20
|
+
hidden?: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type RowColumn<Row> = {
|
|
23
|
+
[K in keyof Row]-?: TypedColumn<Row, K>;
|
|
24
|
+
}[keyof Row];
|
|
25
|
+
type Key<Row> = keyof Row & string;
|
|
26
|
+
type FileSource<Row> = {
|
|
27
|
+
url: Key<Row>;
|
|
28
|
+
} | {
|
|
29
|
+
fetch: true;
|
|
30
|
+
};
|
|
31
|
+
export type FileTableSpec<Row> = {
|
|
32
|
+
title?: string;
|
|
33
|
+
name: Key<Row>;
|
|
34
|
+
source: FileSource<Row>;
|
|
35
|
+
ext?: string;
|
|
36
|
+
category?: string;
|
|
37
|
+
folder?: Key<Row>;
|
|
38
|
+
columns?: Key<Row>[];
|
|
39
|
+
groupBy?: Key<Row>;
|
|
40
|
+
};
|
|
41
|
+
type SummarySpec<Row> = {
|
|
42
|
+
section: Section;
|
|
43
|
+
label: string;
|
|
44
|
+
value: number;
|
|
45
|
+
role?: 'money' | 'count' | 'percent';
|
|
46
|
+
currency?: string;
|
|
47
|
+
basis?: Summary['basis'];
|
|
48
|
+
x?: Key<Row>;
|
|
49
|
+
y?: Key<Row>;
|
|
50
|
+
};
|
|
51
|
+
export type RowDetailSpec<Row> = {
|
|
52
|
+
rows: TableHandle<unknown>;
|
|
53
|
+
on: Key<Row>;
|
|
54
|
+
};
|
|
55
|
+
export interface TableHandle<Row> {
|
|
56
|
+
dataset: TableDataset;
|
|
57
|
+
table(opts?: {
|
|
58
|
+
title?: string;
|
|
59
|
+
columns?: Key<Row>[];
|
|
60
|
+
groupBy?: Key<Row>;
|
|
61
|
+
detail?: RowDetailSpec<Row>;
|
|
62
|
+
}): ViewSpec;
|
|
63
|
+
fileTable(opts: FileTableSpec<Row>): ViewSpec;
|
|
64
|
+
timeseries(opts: {
|
|
65
|
+
x: Key<Row>;
|
|
66
|
+
y: Key<Row>;
|
|
67
|
+
granularity?: 'monthly' | 'daily';
|
|
68
|
+
stackBy?: Key<Row>;
|
|
69
|
+
title?: string;
|
|
70
|
+
}): ViewSpec;
|
|
71
|
+
summary(opts: SummarySpec<Row>): Summary;
|
|
72
|
+
}
|
|
73
|
+
export type StatFieldSpec<Row> = {
|
|
74
|
+
key: Key<Row>;
|
|
75
|
+
max?: number;
|
|
76
|
+
unit?: string;
|
|
77
|
+
caption?: string;
|
|
78
|
+
tone?: 'positive' | 'negative' | 'muted';
|
|
79
|
+
};
|
|
80
|
+
export interface RecordHandle<Row> {
|
|
81
|
+
dataset: RecordDataset;
|
|
82
|
+
stat(opts?: {
|
|
83
|
+
fields?: (Key<Row> | StatFieldSpec<Row>)[];
|
|
84
|
+
title?: string;
|
|
85
|
+
}): ViewSpec;
|
|
86
|
+
keyvalue(opts?: {
|
|
87
|
+
title?: string;
|
|
88
|
+
}): ViewSpec;
|
|
89
|
+
}
|
|
90
|
+
export declare const table: <Row extends object>(spec: {
|
|
91
|
+
id: string;
|
|
92
|
+
columns: RowColumn<Row>[];
|
|
93
|
+
rows: Row[];
|
|
94
|
+
key?: Key<Row> | Key<Row>[];
|
|
95
|
+
}) => TableHandle<Row>;
|
|
96
|
+
export declare const record: (<Row extends object>(spec: {
|
|
97
|
+
id: string;
|
|
98
|
+
fields: RowColumn<Row>[];
|
|
99
|
+
value: Row;
|
|
100
|
+
}) => RecordHandle<Row>) & {
|
|
101
|
+
fromColumns: (spec: {
|
|
102
|
+
id: string;
|
|
103
|
+
fields: Column[];
|
|
104
|
+
value: Record<string, unknown>;
|
|
105
|
+
}) => RecordHandle<Record<string, unknown>>;
|
|
106
|
+
};
|
|
107
|
+
export declare const capabilityResult: (spec: {
|
|
108
|
+
sections: (ViewSpec | null | undefined | false)[];
|
|
109
|
+
summaries?: Summary[];
|
|
110
|
+
}) => CapabilityResult;
|
|
111
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { uniqBy } from 'lodash-es';
|
|
2
|
+
import { omitUndef } from '../util/object.js';
|
|
3
|
+
import { rawRecord, rawTable } from './dataset.js';
|
|
4
|
+
// `Row extends object` (not `Record<string, unknown>`) so an `interface` row type is accepted: TS interfaces
|
|
5
|
+
// lack an implicit index signature, so they fail the Record constraint even though their fields are known.
|
|
6
|
+
export const table = (spec) => {
|
|
7
|
+
// The typed builder is the only public way to construct a dataset; it delegates the wire-object shape to
|
|
8
|
+
// the low-level rawTable so there's a single place that knows the TableDataset layout.
|
|
9
|
+
const dataset = rawTable(spec.id, spec.columns, spec.rows, spec.key);
|
|
10
|
+
// Each method annotates its view against the matching View union variant before omitUndef strips the
|
|
11
|
+
// undefined keys — the annotation structurally checks the builder's field set, so a drift between what the
|
|
12
|
+
// builder emits and the wire schema fails to compile rather than slipping through a cast.
|
|
13
|
+
return {
|
|
14
|
+
dataset,
|
|
15
|
+
table: (opts = {}) => {
|
|
16
|
+
const view = {
|
|
17
|
+
type: 'table',
|
|
18
|
+
dataset: spec.id,
|
|
19
|
+
title: opts.title,
|
|
20
|
+
columns: opts.columns,
|
|
21
|
+
groupBy: opts.groupBy,
|
|
22
|
+
detail: opts.detail ? { dataset: opts.detail.rows.dataset.id, on: opts.detail.on } : undefined
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
dataset,
|
|
26
|
+
view: omitUndef(view),
|
|
27
|
+
...(opts.detail ? { extraDatasets: [opts.detail.rows.dataset] } : {})
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
fileTable: (opts) => {
|
|
31
|
+
const view = {
|
|
32
|
+
type: 'table',
|
|
33
|
+
dataset: spec.id,
|
|
34
|
+
title: opts.title,
|
|
35
|
+
columns: opts.columns,
|
|
36
|
+
groupBy: opts.groupBy,
|
|
37
|
+
files: omitUndef({
|
|
38
|
+
name: opts.name,
|
|
39
|
+
source: opts.source,
|
|
40
|
+
ext: opts.ext,
|
|
41
|
+
category: opts.category,
|
|
42
|
+
folder: opts.folder
|
|
43
|
+
})
|
|
44
|
+
};
|
|
45
|
+
return { dataset, view: omitUndef(view) };
|
|
46
|
+
},
|
|
47
|
+
timeseries: (opts) => {
|
|
48
|
+
const view = {
|
|
49
|
+
type: 'timeseries',
|
|
50
|
+
dataset: spec.id,
|
|
51
|
+
x: opts.x,
|
|
52
|
+
y: opts.y,
|
|
53
|
+
granularity: opts.granularity,
|
|
54
|
+
stackBy: opts.stackBy,
|
|
55
|
+
title: opts.title
|
|
56
|
+
};
|
|
57
|
+
return { dataset, view: omitUndef(view) };
|
|
58
|
+
},
|
|
59
|
+
summary: (opts) => {
|
|
60
|
+
const summary = {
|
|
61
|
+
section: opts.section,
|
|
62
|
+
label: opts.label,
|
|
63
|
+
value: opts.value,
|
|
64
|
+
role: opts.role ?? 'money',
|
|
65
|
+
currency: opts.currency,
|
|
66
|
+
basis: opts.basis,
|
|
67
|
+
spark: opts.x && opts.y ? { dataset: spec.id, x: opts.x, y: opts.y } : undefined
|
|
68
|
+
};
|
|
69
|
+
return omitUndef(summary);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
// The stat/keyvalue handle off an already-built record dataset — shared by the typed `record` and the
|
|
74
|
+
// dynamic `record.fromColumns`.
|
|
75
|
+
const recordHandle = (dataset) => ({
|
|
76
|
+
dataset,
|
|
77
|
+
stat: (opts = {}) => {
|
|
78
|
+
const fields = opts.fields?.map((f) => (typeof f === 'string' ? f : omitUndef(f)));
|
|
79
|
+
const view = { type: 'stat', dataset: dataset.id, fields, title: opts.title };
|
|
80
|
+
return { dataset, view: omitUndef(view) };
|
|
81
|
+
},
|
|
82
|
+
keyvalue: (opts = {}) => {
|
|
83
|
+
const view = { type: 'keyvalue', dataset: dataset.id, title: opts.title };
|
|
84
|
+
return { dataset, view: omitUndef(view) };
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// A record dataset. `record({ id, fields, value })` types the columns against a statically-known row.
|
|
88
|
+
// `record.fromColumns({ id, fields, value })` is the dynamic form for a runtime-assembled `Column[]` + value
|
|
89
|
+
// object (a preset building an account/credits/payment-method record from optional fields) — no per-row
|
|
90
|
+
// generic, no cast at the call site.
|
|
91
|
+
export const record = Object.assign((spec) => recordHandle(rawRecord(spec.id, spec.fields, spec.value)), {
|
|
92
|
+
fromColumns: (spec) => recordHandle(rawRecord(spec.id, spec.fields, spec.value))
|
|
93
|
+
});
|
|
94
|
+
// Assemble a CapabilityResult from view-specs: collect each section's dataset (de-duped by id, first wins),
|
|
95
|
+
// drop falsy sections (conditional panels), and attach optional summaries. Section order is literal array order.
|
|
96
|
+
export const capabilityResult = (spec) => {
|
|
97
|
+
const sections = spec.sections.filter((s) => Boolean(s));
|
|
98
|
+
return {
|
|
99
|
+
datasets: uniqBy(sections.flatMap((s) => [s.dataset, ...(s.extraDatasets ?? [])]), (d) => d.id),
|
|
100
|
+
views: sections.map((s) => s.view),
|
|
101
|
+
...(spec.summaries?.length ? { summaries: spec.summaries } : {})
|
|
102
|
+
};
|
|
103
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { CapabilityResult } from './result.js';
|
|
2
|
+
export type CurrencyCode = 'USD' | 'CAD' | 'EUR' | 'GBP' | 'AUD' | 'NZD' | 'JPY' | 'CHF' | 'CNY' | 'INR' | 'BRL' | 'MXN' | 'SEK' | 'NOK' | 'DKK' | 'SGD' | 'HKD' | 'ZAR' | 'PLN';
|
|
3
|
+
export declare const resolveCurrencies: (result: CapabilityResult, reportingCurrency: CurrencyCode) => CapabilityResult;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Fill the concrete currency onto every money value that did not declare one, so the persisted report is
|
|
2
|
+
// self-describing and nothing downstream falls back to a guessed default. A money value's own `currency`
|
|
3
|
+
// (a per-value override) always wins; non-money roles are never touched.
|
|
4
|
+
const stampColumns = (cols, reportingCurrency) => cols.map((c) => (c.role === 'money' && !c.currency ? { ...c, currency: reportingCurrency } : c));
|
|
5
|
+
export const resolveCurrencies = (result, reportingCurrency) => {
|
|
6
|
+
const datasets = result.datasets.map((ds) => ds.shape === 'table'
|
|
7
|
+
? { ...ds, columns: stampColumns(ds.columns, reportingCurrency) }
|
|
8
|
+
: { ...ds, fields: stampColumns(ds.fields, reportingCurrency) });
|
|
9
|
+
const summaries = result.summaries?.map((s) => s.role === 'money' && !s.currency ? { ...s, currency: reportingCurrency } : s);
|
|
10
|
+
return { ...result, datasets, ...(summaries?.length ? { summaries } : {}) };
|
|
11
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const SemanticRoleSchema: z.ZodEnum<{
|
|
3
|
+
category: "category";
|
|
4
|
+
count: "count";
|
|
5
|
+
identifier: "identifier";
|
|
6
|
+
label: "label";
|
|
7
|
+
money: "money";
|
|
8
|
+
percent: "percent";
|
|
9
|
+
status: "status";
|
|
10
|
+
text: "text";
|
|
11
|
+
timestamp: "timestamp";
|
|
12
|
+
url: "url";
|
|
13
|
+
}>;
|
|
14
|
+
export type SemanticRole = z.infer<typeof SemanticRoleSchema>;
|
|
15
|
+
export declare const BadgeToneSchema: z.ZodEnum<{
|
|
16
|
+
danger: "danger";
|
|
17
|
+
info: "info";
|
|
18
|
+
neutral: "neutral";
|
|
19
|
+
success: "success";
|
|
20
|
+
warning: "warning";
|
|
21
|
+
}>;
|
|
22
|
+
export type BadgeTone = z.infer<typeof BadgeToneSchema>;
|
|
23
|
+
export declare const AccrualSchema: z.ZodEnum<{
|
|
24
|
+
cumulative: "cumulative";
|
|
25
|
+
incremental: "incremental";
|
|
26
|
+
}>;
|
|
27
|
+
export type Accrual = z.infer<typeof AccrualSchema>;
|
|
28
|
+
export declare const ResetPeriodSchema: z.ZodEnum<{
|
|
29
|
+
monthly: "monthly";
|
|
30
|
+
none: "none";
|
|
31
|
+
}>;
|
|
32
|
+
export type ResetPeriod = z.infer<typeof ResetPeriodSchema>;
|
|
33
|
+
export declare const ColumnSchema: z.ZodObject<{
|
|
34
|
+
key: z.ZodString;
|
|
35
|
+
label: z.ZodOptional<z.ZodString>;
|
|
36
|
+
role: z.ZodEnum<{
|
|
37
|
+
category: "category";
|
|
38
|
+
count: "count";
|
|
39
|
+
identifier: "identifier";
|
|
40
|
+
label: "label";
|
|
41
|
+
money: "money";
|
|
42
|
+
percent: "percent";
|
|
43
|
+
status: "status";
|
|
44
|
+
text: "text";
|
|
45
|
+
timestamp: "timestamp";
|
|
46
|
+
url: "url";
|
|
47
|
+
}>;
|
|
48
|
+
currency: z.ZodOptional<z.ZodString>;
|
|
49
|
+
badges: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
50
|
+
danger: "danger";
|
|
51
|
+
info: "info";
|
|
52
|
+
neutral: "neutral";
|
|
53
|
+
success: "success";
|
|
54
|
+
warning: "warning";
|
|
55
|
+
}>>>;
|
|
56
|
+
accrual: z.ZodOptional<z.ZodEnum<{
|
|
57
|
+
cumulative: "cumulative";
|
|
58
|
+
incremental: "incremental";
|
|
59
|
+
}>>;
|
|
60
|
+
resetPeriod: z.ZodOptional<z.ZodEnum<{
|
|
61
|
+
monthly: "monthly";
|
|
62
|
+
none: "none";
|
|
63
|
+
}>>;
|
|
64
|
+
truncate: z.ZodOptional<z.ZodBoolean>;
|
|
65
|
+
hidden: z.ZodOptional<z.ZodBoolean>;
|
|
66
|
+
}, z.core.$strip>;
|
|
67
|
+
export type Column = z.infer<typeof ColumnSchema>;
|
|
68
|
+
export declare const TableDatasetSchema: z.ZodObject<{
|
|
69
|
+
id: z.ZodString;
|
|
70
|
+
shape: z.ZodLiteral<"table">;
|
|
71
|
+
columns: z.ZodArray<z.ZodObject<{
|
|
72
|
+
key: z.ZodString;
|
|
73
|
+
label: z.ZodOptional<z.ZodString>;
|
|
74
|
+
role: z.ZodEnum<{
|
|
75
|
+
category: "category";
|
|
76
|
+
count: "count";
|
|
77
|
+
identifier: "identifier";
|
|
78
|
+
label: "label";
|
|
79
|
+
money: "money";
|
|
80
|
+
percent: "percent";
|
|
81
|
+
status: "status";
|
|
82
|
+
text: "text";
|
|
83
|
+
timestamp: "timestamp";
|
|
84
|
+
url: "url";
|
|
85
|
+
}>;
|
|
86
|
+
currency: z.ZodOptional<z.ZodString>;
|
|
87
|
+
badges: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
88
|
+
danger: "danger";
|
|
89
|
+
info: "info";
|
|
90
|
+
neutral: "neutral";
|
|
91
|
+
success: "success";
|
|
92
|
+
warning: "warning";
|
|
93
|
+
}>>>;
|
|
94
|
+
accrual: z.ZodOptional<z.ZodEnum<{
|
|
95
|
+
cumulative: "cumulative";
|
|
96
|
+
incremental: "incremental";
|
|
97
|
+
}>>;
|
|
98
|
+
resetPeriod: z.ZodOptional<z.ZodEnum<{
|
|
99
|
+
monthly: "monthly";
|
|
100
|
+
none: "none";
|
|
101
|
+
}>>;
|
|
102
|
+
truncate: z.ZodOptional<z.ZodBoolean>;
|
|
103
|
+
hidden: z.ZodOptional<z.ZodBoolean>;
|
|
104
|
+
}, z.core.$strip>>;
|
|
105
|
+
rows: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
106
|
+
key: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
107
|
+
}, z.core.$strip>;
|
|
108
|
+
export type TableDataset = z.infer<typeof TableDatasetSchema>;
|
|
109
|
+
export declare const RecordDatasetSchema: z.ZodObject<{
|
|
110
|
+
id: z.ZodString;
|
|
111
|
+
shape: z.ZodLiteral<"record">;
|
|
112
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
113
|
+
key: z.ZodString;
|
|
114
|
+
label: z.ZodOptional<z.ZodString>;
|
|
115
|
+
role: z.ZodEnum<{
|
|
116
|
+
category: "category";
|
|
117
|
+
count: "count";
|
|
118
|
+
identifier: "identifier";
|
|
119
|
+
label: "label";
|
|
120
|
+
money: "money";
|
|
121
|
+
percent: "percent";
|
|
122
|
+
status: "status";
|
|
123
|
+
text: "text";
|
|
124
|
+
timestamp: "timestamp";
|
|
125
|
+
url: "url";
|
|
126
|
+
}>;
|
|
127
|
+
currency: z.ZodOptional<z.ZodString>;
|
|
128
|
+
badges: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
129
|
+
danger: "danger";
|
|
130
|
+
info: "info";
|
|
131
|
+
neutral: "neutral";
|
|
132
|
+
success: "success";
|
|
133
|
+
warning: "warning";
|
|
134
|
+
}>>>;
|
|
135
|
+
accrual: z.ZodOptional<z.ZodEnum<{
|
|
136
|
+
cumulative: "cumulative";
|
|
137
|
+
incremental: "incremental";
|
|
138
|
+
}>>;
|
|
139
|
+
resetPeriod: z.ZodOptional<z.ZodEnum<{
|
|
140
|
+
monthly: "monthly";
|
|
141
|
+
none: "none";
|
|
142
|
+
}>>;
|
|
143
|
+
truncate: z.ZodOptional<z.ZodBoolean>;
|
|
144
|
+
hidden: z.ZodOptional<z.ZodBoolean>;
|
|
145
|
+
}, z.core.$strip>>;
|
|
146
|
+
value: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
147
|
+
}, z.core.$strip>;
|
|
148
|
+
export type RecordDataset = z.infer<typeof RecordDatasetSchema>;
|
|
149
|
+
export declare const DatasetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
150
|
+
id: z.ZodString;
|
|
151
|
+
shape: z.ZodLiteral<"table">;
|
|
152
|
+
columns: z.ZodArray<z.ZodObject<{
|
|
153
|
+
key: z.ZodString;
|
|
154
|
+
label: z.ZodOptional<z.ZodString>;
|
|
155
|
+
role: z.ZodEnum<{
|
|
156
|
+
category: "category";
|
|
157
|
+
count: "count";
|
|
158
|
+
identifier: "identifier";
|
|
159
|
+
label: "label";
|
|
160
|
+
money: "money";
|
|
161
|
+
percent: "percent";
|
|
162
|
+
status: "status";
|
|
163
|
+
text: "text";
|
|
164
|
+
timestamp: "timestamp";
|
|
165
|
+
url: "url";
|
|
166
|
+
}>;
|
|
167
|
+
currency: z.ZodOptional<z.ZodString>;
|
|
168
|
+
badges: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
169
|
+
danger: "danger";
|
|
170
|
+
info: "info";
|
|
171
|
+
neutral: "neutral";
|
|
172
|
+
success: "success";
|
|
173
|
+
warning: "warning";
|
|
174
|
+
}>>>;
|
|
175
|
+
accrual: z.ZodOptional<z.ZodEnum<{
|
|
176
|
+
cumulative: "cumulative";
|
|
177
|
+
incremental: "incremental";
|
|
178
|
+
}>>;
|
|
179
|
+
resetPeriod: z.ZodOptional<z.ZodEnum<{
|
|
180
|
+
monthly: "monthly";
|
|
181
|
+
none: "none";
|
|
182
|
+
}>>;
|
|
183
|
+
truncate: z.ZodOptional<z.ZodBoolean>;
|
|
184
|
+
hidden: z.ZodOptional<z.ZodBoolean>;
|
|
185
|
+
}, z.core.$strip>>;
|
|
186
|
+
rows: z.ZodArray<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
187
|
+
key: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>;
|
|
188
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
189
|
+
id: z.ZodString;
|
|
190
|
+
shape: z.ZodLiteral<"record">;
|
|
191
|
+
fields: z.ZodArray<z.ZodObject<{
|
|
192
|
+
key: z.ZodString;
|
|
193
|
+
label: z.ZodOptional<z.ZodString>;
|
|
194
|
+
role: z.ZodEnum<{
|
|
195
|
+
category: "category";
|
|
196
|
+
count: "count";
|
|
197
|
+
identifier: "identifier";
|
|
198
|
+
label: "label";
|
|
199
|
+
money: "money";
|
|
200
|
+
percent: "percent";
|
|
201
|
+
status: "status";
|
|
202
|
+
text: "text";
|
|
203
|
+
timestamp: "timestamp";
|
|
204
|
+
url: "url";
|
|
205
|
+
}>;
|
|
206
|
+
currency: z.ZodOptional<z.ZodString>;
|
|
207
|
+
badges: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
208
|
+
danger: "danger";
|
|
209
|
+
info: "info";
|
|
210
|
+
neutral: "neutral";
|
|
211
|
+
success: "success";
|
|
212
|
+
warning: "warning";
|
|
213
|
+
}>>>;
|
|
214
|
+
accrual: z.ZodOptional<z.ZodEnum<{
|
|
215
|
+
cumulative: "cumulative";
|
|
216
|
+
incremental: "incremental";
|
|
217
|
+
}>>;
|
|
218
|
+
resetPeriod: z.ZodOptional<z.ZodEnum<{
|
|
219
|
+
monthly: "monthly";
|
|
220
|
+
none: "none";
|
|
221
|
+
}>>;
|
|
222
|
+
truncate: z.ZodOptional<z.ZodBoolean>;
|
|
223
|
+
hidden: z.ZodOptional<z.ZodBoolean>;
|
|
224
|
+
}, z.core.$strip>>;
|
|
225
|
+
value: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
226
|
+
}, z.core.$strip>], "shape">;
|
|
227
|
+
export type Dataset = z.infer<typeof DatasetSchema>;
|
|
228
|
+
export declare const rawTable: (id: string, columns: Column[], rows: Record<string, unknown>[], key?: string | string[]) => TableDataset;
|
|
229
|
+
export declare const rawRecord: (id: string, fields: Column[], value: Record<string, unknown>) => RecordDataset;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// The semantic role tells the renderer what a column MEANS (money → formatUsd, timestamp → chart axis,
|
|
3
|
+
// status → sentiment badge, category → distinct-hue badge, url → link) and lets cross-service rollups find
|
|
4
|
+
// comparable fields. The two badge roles split by intent: `status` carries SENTIMENT (active is good, failed
|
|
5
|
+
// is bad) — the renderer auto-tones a known vocabulary; `category` carries NO sentiment (a member role, a
|
|
6
|
+
// seat tier) — the renderer assigns each value a stable distinct hue. `label` is plain text.
|
|
7
|
+
export const SemanticRoleSchema = z.enum([
|
|
8
|
+
'money',
|
|
9
|
+
'count',
|
|
10
|
+
'percent',
|
|
11
|
+
'timestamp',
|
|
12
|
+
'status',
|
|
13
|
+
'category',
|
|
14
|
+
'label',
|
|
15
|
+
'identifier',
|
|
16
|
+
'url',
|
|
17
|
+
'text'
|
|
18
|
+
]);
|
|
19
|
+
// The sentiment a status value carries. This is MEANING, not color — the renderer maps each to a themed
|
|
20
|
+
// badge variant. A plugin declares a tone only to override or extend what the renderer already infers from a
|
|
21
|
+
// status value (categorical coloring for role:'category' values is automatic — the plugin picks no color).
|
|
22
|
+
export const BadgeToneSchema = z.enum(['neutral', 'success', 'warning', 'danger', 'info']);
|
|
23
|
+
// How a column's value accrues over time. 'cumulative' is a counter that resets each `resetPeriod` (an MTD
|
|
24
|
+
// total) — core keeps a per-row daily reading series and differences it into a daily breakdown. 'incremental'
|
|
25
|
+
// (the default) is a point-in-time/period amount kept verbatim.
|
|
26
|
+
export const AccrualSchema = z.enum(['cumulative', 'incremental']);
|
|
27
|
+
export const ResetPeriodSchema = z.enum(['monthly', 'none']);
|
|
28
|
+
export const ColumnSchema = z.object({
|
|
29
|
+
key: z.string(),
|
|
30
|
+
label: z.string().optional(),
|
|
31
|
+
role: SemanticRoleSchema,
|
|
32
|
+
currency: z.string().optional(),
|
|
33
|
+
// value (matched case-insensitively) → sentiment tone, OVERRIDING the renderer's inferred tone for a
|
|
34
|
+
// role:'status' column. Only needed for values the renderer can't infer (service-specific or non-English
|
|
35
|
+
// statuses); common ones (active/paid/failed/…) auto-tone with no map. Ignored for other roles.
|
|
36
|
+
badges: z.record(z.string(), BadgeToneSchema).optional(),
|
|
37
|
+
accrual: AccrualSchema.optional(),
|
|
38
|
+
resetPeriod: ResetPeriodSchema.optional(),
|
|
39
|
+
// Cap a long free-text column to one ellipsized line so it stops absorbing the row's slack and squashing
|
|
40
|
+
// its neighbours. The full value stays reachable: a hover peek (native title) plus a click-to-open popover
|
|
41
|
+
// with the selectable, copyable text. Opt-in per column; only meaningful for free-text roles (label/text).
|
|
42
|
+
truncate: z.boolean().optional(),
|
|
43
|
+
// Declared but not for display: a row field the table carries for machinery (accumulation key, download
|
|
44
|
+
// filename, fetchFile payload) rather than for showing in the table body.
|
|
45
|
+
hidden: z.boolean().optional()
|
|
46
|
+
});
|
|
47
|
+
export const TableDatasetSchema = z.object({
|
|
48
|
+
id: z.string(),
|
|
49
|
+
shape: z.literal('table'),
|
|
50
|
+
columns: z.array(ColumnSchema),
|
|
51
|
+
rows: z.array(z.record(z.string(), z.unknown())),
|
|
52
|
+
// Column(s) forming a row's stable identity across fetches. Its presence opts the table into accumulation
|
|
53
|
+
// (rows merge into a kept union instead of overwriting); an array is a composite key. Absent → latest-only.
|
|
54
|
+
key: z.union([z.string(), z.array(z.string())]).optional()
|
|
55
|
+
});
|
|
56
|
+
export const RecordDatasetSchema = z.object({
|
|
57
|
+
id: z.string(),
|
|
58
|
+
shape: z.literal('record'),
|
|
59
|
+
fields: z.array(ColumnSchema),
|
|
60
|
+
value: z.record(z.string(), z.unknown())
|
|
61
|
+
});
|
|
62
|
+
// Two structural primitives; a series is a table with a timestamp column, a stat is a record field.
|
|
63
|
+
export const DatasetSchema = z.discriminatedUnion('shape', [TableDatasetSchema, RecordDatasetSchema]);
|
|
64
|
+
export const rawTable = (id, columns, rows, key) => ({ id, shape: 'table', columns, rows, ...(key ? { key } : {}) });
|
|
65
|
+
export const rawRecord = (id, fields, value) => ({
|
|
66
|
+
id,
|
|
67
|
+
shape: 'record',
|
|
68
|
+
fields,
|
|
69
|
+
value
|
|
70
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { rawTable, rawRecord } from './dataset.js';
|
|
2
|
+
export type { SemanticRole, BadgeTone, Accrual, ResetPeriod, Column, TableDataset, RecordDataset, Dataset } from './dataset.js';
|
|
3
|
+
export type { MonthPoint } from './series.js';
|
|
4
|
+
export type { RolesFor } from './roles.js';
|
|
5
|
+
export { table, record, capabilityResult } from './builders.js';
|
|
6
|
+
export type { ViewSpec, RowColumn, FileTableSpec, TableHandle, StatFieldSpec, RecordHandle } from './builders.js';
|
|
7
|
+
export { resolveTableFiles } from './view.js';
|
|
8
|
+
export type { FileSource, TableFiles, StatTone, StatField, View } from './view.js';
|
|
9
|
+
export type { Section, Summary } from './summary.js';
|
|
10
|
+
export { validateCapabilityResult } from './result.js';
|
|
11
|
+
export type { CapabilityResult } from './result.js';
|
|
12
|
+
export { resolveCurrencies } from './currency.js';
|
|
13
|
+
export type { CurrencyCode } from './currency.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @butinapp/sdk/data — the data-view contract a capability's collect()/build() emits: datasets (table ·
|
|
2
|
+
// record with semantic-role columns), views, summaries, the typed builders (table · record ·
|
|
3
|
+
// capabilityResult), and the currency/role/series shapes they compose. The plugin DESCRIPTOR (definePlugin ·
|
|
4
|
+
// defineCapability · meta/session/auth/transport/config) is the root '@butinapp/sdk' — this is the separate
|
|
5
|
+
// "what a capability returns" tier. Prefer the high-altitude presets ('@butinapp/sdk/presets') over
|
|
6
|
+
// hand-building datasets/views. The internal zod *Schema objects stay off this surface — annotate the TYPES.
|
|
7
|
+
export { rawTable, rawRecord } from './dataset.js';
|
|
8
|
+
export { table, record, capabilityResult } from './builders.js';
|
|
9
|
+
export { resolveTableFiles } from './view.js';
|
|
10
|
+
export { validateCapabilityResult } from './result.js';
|
|
11
|
+
export { resolveCurrencies } from './currency.js';
|