@arcote.tech/arc-ai 0.5.1 → 0.5.5
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/package.json +3 -3
- package/src/aggregates/credit-ledger.ts +178 -0
- package/src/ai-builder.ts +19 -36
- package/src/billing/index.ts +5 -0
- package/src/billing/strategies/fixed-markup.ts +32 -0
- package/src/billing/strategies/flat-rate.ts +19 -0
- package/src/billing/strategies/index.ts +3 -0
- package/src/billing/strategies/token-pass.ts +15 -0
- package/src/billing/types.ts +24 -0
- package/src/billing/wrap-provider.ts +52 -0
- package/src/index.ts +21 -9
- package/src/tool/tool.ts +139 -53
- package/src/types.ts +100 -23
- package/src/aggregates/budget.ts +0 -137
- package/src/aggregates/completion.ts +0 -235
- package/src/routes/stream-completion.ts +0 -94
package/src/tool/tool.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
arcFunctionWithCtx,
|
|
3
|
+
type ArcFunction,
|
|
4
|
+
type ArcId,
|
|
5
5
|
type ArcRawShape,
|
|
6
|
-
type $type,
|
|
7
6
|
type ArcContextElement,
|
|
8
7
|
type ArcFunctionData,
|
|
8
|
+
type ArcObjectAny,
|
|
9
|
+
type FnContext,
|
|
10
|
+
type Merge,
|
|
11
|
+
type $type,
|
|
12
|
+
type DefaultFunctionData,
|
|
13
|
+
ArcObject,
|
|
9
14
|
} from "@arcote.tech/arc";
|
|
15
|
+
import type { ComponentType } from "react";
|
|
10
16
|
|
|
11
17
|
// ─── JSON Schema Tool Definition (sent to LLM) ──────────────────
|
|
12
18
|
|
|
@@ -16,68 +22,153 @@ export interface JsonSchemaToolDef {
|
|
|
16
22
|
parameters: Record<string, unknown>;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
// ───
|
|
25
|
+
// ─── View Props ─────────────────────────────────────────────────
|
|
20
26
|
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
export type ServerToolViewProps<P, R> = {
|
|
28
|
+
params: P;
|
|
29
|
+
result: R | undefined;
|
|
30
|
+
calling: boolean;
|
|
31
|
+
error?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type InteractiveToolViewProps<P, R> = {
|
|
35
|
+
params: P;
|
|
36
|
+
respond: (result: R) => void;
|
|
37
|
+
calling: boolean;
|
|
38
|
+
};
|
|
26
39
|
|
|
27
40
|
// ─── ArcTool ─────────────────────────────────────────────────────
|
|
28
41
|
|
|
29
42
|
export class ArcTool<
|
|
30
43
|
const Name extends string = string,
|
|
31
|
-
|
|
44
|
+
Data extends ArcFunctionData = DefaultFunctionData,
|
|
45
|
+
IdentifyBy = string,
|
|
32
46
|
> {
|
|
33
47
|
readonly name: Name;
|
|
34
|
-
#fn: ArcFunction<
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
constructor(
|
|
48
|
+
readonly #fn: ArcFunction<Data, { identifyBy: IdentifyBy }>;
|
|
49
|
+
readonly #view?: ComponentType<any>;
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
name: Name,
|
|
53
|
+
fn?: ArcFunction<any, any>,
|
|
54
|
+
view?: ComponentType<any>,
|
|
55
|
+
) {
|
|
38
56
|
this.name = name;
|
|
39
|
-
this.#fn = fn ??
|
|
57
|
+
this.#fn = fn ?? (arcFunctionWithCtx<{ identifyBy: IdentifyBy }>() as any);
|
|
58
|
+
this.#view = view;
|
|
40
59
|
}
|
|
41
60
|
|
|
61
|
+
// ─── Builder methods ──────────────────────────────────────────
|
|
62
|
+
|
|
42
63
|
description<const D extends string>(desc: D) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
const nextFn = this.#fn.description(desc);
|
|
65
|
+
return new ArcTool<Name, Merge<Data, { description: D }>, IdentifyBy>(
|
|
66
|
+
this.name, nextFn as any, this.#view,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
withParams<const S extends ArcRawShape>(shape: S) {
|
|
71
|
+
const nextFn = this.#fn.withParams(shape);
|
|
72
|
+
return new ArcTool<Name, Merge<Data, { params: ArcObject<S> }>, IdentifyBy>(
|
|
73
|
+
this.name, nextFn as any, this.#view,
|
|
74
|
+
);
|
|
46
75
|
}
|
|
47
76
|
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
return
|
|
77
|
+
withResult<const S extends ArcRawShape>(shape: S) {
|
|
78
|
+
const nextFn = this.#fn.withResult(shape);
|
|
79
|
+
return new ArcTool<Name, Merge<Data, { result: ArcObject<S> }>, IdentifyBy>(
|
|
80
|
+
this.name, nextFn as any, this.#view,
|
|
81
|
+
);
|
|
51
82
|
}
|
|
52
83
|
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
84
|
+
query<const Elements extends ArcContextElement<any>[]>(elements: Elements) {
|
|
85
|
+
const nextFn = this.#fn.query(elements);
|
|
86
|
+
return new ArcTool<Name, Merge<Data, { queryElements: Elements }>, IdentifyBy>(
|
|
87
|
+
this.name, nextFn as any, this.#view,
|
|
88
|
+
);
|
|
57
89
|
}
|
|
58
90
|
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
91
|
+
mutate<const Elements extends ArcContextElement<any>[]>(elements: Elements) {
|
|
92
|
+
const nextFn = this.#fn.mutate(elements);
|
|
93
|
+
return new ArcTool<Name, Merge<Data, { mutationElements: Elements }>, IdentifyBy>(
|
|
94
|
+
this.name, nextFn as any, this.#view,
|
|
95
|
+
);
|
|
63
96
|
}
|
|
64
97
|
|
|
98
|
+
identifyBy<Id extends ArcId<any>>(_id: Id) {
|
|
99
|
+
return new ArcTool<Name, Data, $type<Id>>(
|
|
100
|
+
this.name, this.#fn as any, this.#view,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
handle<
|
|
105
|
+
Handler extends
|
|
106
|
+
| ((
|
|
107
|
+
ctx: FnContext<Data, { identifyBy: IdentifyBy }>,
|
|
108
|
+
...args: Data["params"] extends ArcObjectAny
|
|
109
|
+
? [$type<Data["params"]>]
|
|
110
|
+
: []
|
|
111
|
+
) => Promise<any>)
|
|
112
|
+
| false,
|
|
113
|
+
>(handler: Handler) {
|
|
114
|
+
const nextFn = this.#fn.handle(handler as any);
|
|
115
|
+
return new ArcTool<Name, Merge<Data, { handler: Handler }>, IdentifyBy>(
|
|
116
|
+
this.name, nextFn as any, this.#view,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
view(
|
|
121
|
+
component: ComponentType<
|
|
122
|
+
Data["handler"] extends Function
|
|
123
|
+
? ServerToolViewProps<
|
|
124
|
+
Data["params"] extends ArcObjectAny ? $type<Data["params"]> : {},
|
|
125
|
+
Data["result"] extends ArcObjectAny
|
|
126
|
+
? $type<Data["result"]>
|
|
127
|
+
: unknown
|
|
128
|
+
>
|
|
129
|
+
: InteractiveToolViewProps<
|
|
130
|
+
Data["params"] extends ArcObjectAny ? $type<Data["params"]> : {},
|
|
131
|
+
Data["result"] extends ArcObjectAny
|
|
132
|
+
? $type<Data["result"]>
|
|
133
|
+
: unknown
|
|
134
|
+
>
|
|
135
|
+
>,
|
|
136
|
+
) {
|
|
137
|
+
return new ArcTool<Name, Data, IdentifyBy>(this.name, this.#fn as any, component);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Getters ──────────────────────────────────────────────────
|
|
141
|
+
|
|
65
142
|
get hasHandler(): boolean {
|
|
66
|
-
return !!this.#handler;
|
|
143
|
+
return !!this.#fn.handler;
|
|
67
144
|
}
|
|
68
145
|
|
|
69
146
|
get isServerTool(): boolean {
|
|
70
147
|
return this.hasHandler;
|
|
71
148
|
}
|
|
72
149
|
|
|
73
|
-
get
|
|
74
|
-
return !this.hasHandler;
|
|
150
|
+
get isInteractiveTool(): boolean {
|
|
151
|
+
return !this.hasHandler && !!this.#view;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get hasView(): boolean {
|
|
155
|
+
return !!this.#view;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get viewComponent(): ComponentType<any> | undefined {
|
|
159
|
+
return this.#view;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
get queryElements(): ArcContextElement<any>[] {
|
|
163
|
+
return this.#fn.data.queryElements || [];
|
|
75
164
|
}
|
|
76
165
|
|
|
77
166
|
get mutationElements(): ArcContextElement<any>[] {
|
|
78
167
|
return this.#fn.data.mutationElements || [];
|
|
79
168
|
}
|
|
80
169
|
|
|
170
|
+
// ─── JSON Schema (for LLM) ───────────────────────────────────
|
|
171
|
+
|
|
81
172
|
toJsonSchema(): JsonSchemaToolDef {
|
|
82
173
|
const fnSchema = this.#fn.toJsonSchema();
|
|
83
174
|
return {
|
|
@@ -87,28 +178,23 @@ export class ArcTool<
|
|
|
87
178
|
};
|
|
88
179
|
}
|
|
89
180
|
|
|
90
|
-
|
|
91
|
-
if (!this.#handler) {
|
|
92
|
-
throw new Error(`Tool "${this.name}" has no handler`);
|
|
93
|
-
}
|
|
94
|
-
return this.#handler(ctx, params as $type<ReturnType<typeof object<S>>>);
|
|
95
|
-
}
|
|
181
|
+
// ─── Execution (called by listener) ──────────────────────────
|
|
96
182
|
|
|
97
|
-
async
|
|
98
|
-
|
|
183
|
+
async executeWithContext(
|
|
184
|
+
params: Record<string, unknown>,
|
|
185
|
+
listenerCtx: any,
|
|
186
|
+
identifyByValue: string,
|
|
187
|
+
): Promise<string> {
|
|
188
|
+
const handler = this.#fn.handler as Function | false | null;
|
|
189
|
+
if (!handler) {
|
|
99
190
|
throw new Error(`Tool "${this.name}" has no handler`);
|
|
100
191
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
mutate: () => { throw new Error("No mutation context available"); },
|
|
108
|
-
query: () => { throw new Error("No query context available"); },
|
|
109
|
-
identifyBy: "",
|
|
110
|
-
};
|
|
111
|
-
return this.#handler(emptyCtx, params as $type<ReturnType<typeof object<S>>>);
|
|
192
|
+
|
|
193
|
+
const ctx = { ...listenerCtx, identifyBy: identifyByValue };
|
|
194
|
+
const result = await (handler as Function)(ctx, params);
|
|
195
|
+
|
|
196
|
+
if (typeof result === "string") return result;
|
|
197
|
+
return JSON.stringify(result);
|
|
112
198
|
}
|
|
113
199
|
}
|
|
114
200
|
|
|
@@ -118,4 +204,4 @@ export function tool<const Name extends string>(name: Name) {
|
|
|
118
204
|
return new ArcTool(name);
|
|
119
205
|
}
|
|
120
206
|
|
|
121
|
-
export type ArcToolAny = ArcTool<string,
|
|
207
|
+
export type ArcToolAny = ArcTool<string, any, any>;
|
package/src/types.ts
CHANGED
|
@@ -4,32 +4,92 @@ import type { JsonSchemaToolDef } from "./tool/tool";
|
|
|
4
4
|
|
|
5
5
|
export type ProviderName = "openai" | "gemini" | "claude";
|
|
6
6
|
|
|
7
|
-
// ───
|
|
7
|
+
// ─── Tool Calls ──────────────────────────────────────────────────
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export interface ToolCall {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
arguments: Record<string, unknown>;
|
|
13
|
+
}
|
|
10
14
|
|
|
11
|
-
export interface
|
|
12
|
-
|
|
15
|
+
export interface ToolResult {
|
|
16
|
+
toolCallId: string;
|
|
17
|
+
name: string;
|
|
13
18
|
content: string;
|
|
14
|
-
|
|
15
|
-
toolCallId?: string;
|
|
19
|
+
isError: boolean;
|
|
16
20
|
}
|
|
17
21
|
|
|
18
|
-
// ───
|
|
22
|
+
// ─── Assistant content blocks (ordered) ─────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Real model output interleaves text and tool calls — e.g.
|
|
26
|
+
* "Let me check X" → tool_call → "and now Y" → tool_call. Both OpenAI Responses
|
|
27
|
+
* (output[]) and Claude (content[]) preserve this order. We mirror it 1:1 to
|
|
28
|
+
* avoid losing semantic structure.
|
|
29
|
+
*/
|
|
30
|
+
export interface TextBlock {
|
|
31
|
+
type: "text";
|
|
32
|
+
text: string;
|
|
33
|
+
}
|
|
19
34
|
|
|
20
|
-
export interface
|
|
35
|
+
export interface ToolCallBlock {
|
|
36
|
+
type: "tool_call";
|
|
21
37
|
id: string;
|
|
22
38
|
name: string;
|
|
23
39
|
arguments: Record<string, unknown>;
|
|
24
40
|
}
|
|
25
41
|
|
|
26
|
-
export
|
|
42
|
+
export type AssistantContentBlock = TextBlock | ToolCallBlock;
|
|
43
|
+
|
|
44
|
+
// ─── Conversation turns ─────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
export interface UserTurn {
|
|
47
|
+
role: "user";
|
|
48
|
+
content: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface AssistantTurn {
|
|
52
|
+
role: "assistant";
|
|
53
|
+
/** Ordered model output: text and tool_call blocks interleaved as produced. */
|
|
54
|
+
blocks: AssistantContentBlock[];
|
|
55
|
+
/**
|
|
56
|
+
* Provider-issued response identifier for the turn this represents. Used by
|
|
57
|
+
* adapters that support server-side continuity (OpenAI Responses API) to
|
|
58
|
+
* anchor `previous_response_id` requests. Other adapters ignore this field.
|
|
59
|
+
*/
|
|
60
|
+
responseId?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ToolResultTurn {
|
|
64
|
+
role: "tool_result";
|
|
27
65
|
toolCallId: string;
|
|
28
66
|
name: string;
|
|
29
67
|
content: string;
|
|
30
|
-
isError
|
|
68
|
+
isError?: boolean;
|
|
31
69
|
}
|
|
32
70
|
|
|
71
|
+
export type ConversationTurn = UserTurn | AssistantTurn | ToolResultTurn;
|
|
72
|
+
|
|
73
|
+
// ─── Conversation (full vs continuation) ────────────────────────
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Discriminated union telling the adapter exactly what to send.
|
|
77
|
+
*
|
|
78
|
+
* - `full`: send every turn from `turns`. Used for cold start, or for any
|
|
79
|
+
* provider that does not support server-side continuity (Claude, Gemini).
|
|
80
|
+
*
|
|
81
|
+
* - `continuation`: send only `newTurns` (the delta after the previous
|
|
82
|
+
* response). Used by providers with server-side state (OpenAI Responses API).
|
|
83
|
+
* The provider replays prior context server-side via `previousResponseId`.
|
|
84
|
+
*/
|
|
85
|
+
export type Conversation =
|
|
86
|
+
| { mode: "full"; turns: ConversationTurn[] }
|
|
87
|
+
| {
|
|
88
|
+
mode: "continuation";
|
|
89
|
+
previousResponseId: string;
|
|
90
|
+
newTurns: ConversationTurn[];
|
|
91
|
+
};
|
|
92
|
+
|
|
33
93
|
// ─── Token Usage ─────────────────────────────────────────────────
|
|
34
94
|
|
|
35
95
|
export interface TokenUsage {
|
|
@@ -46,19 +106,35 @@ export type FinishReason = "stop" | "tool_call" | "max_tokens" | "error";
|
|
|
46
106
|
|
|
47
107
|
export interface CompletionRequest {
|
|
48
108
|
model: string;
|
|
49
|
-
|
|
109
|
+
/**
|
|
110
|
+
* System prompt for this turn. Sent on every call. Providers that support
|
|
111
|
+
* server-side continuity (OpenAI Responses API) replace previously stored
|
|
112
|
+
* instructions for the new turn. Required — pass empty string if there is
|
|
113
|
+
* nothing to say.
|
|
114
|
+
*/
|
|
115
|
+
instructions: string;
|
|
116
|
+
conversation: Conversation;
|
|
50
117
|
tools?: JsonSchemaToolDef[];
|
|
118
|
+
toolChoice?: "auto" | "required" | { type: "function"; name: string };
|
|
51
119
|
webSearch?: boolean;
|
|
52
120
|
temperature?: number;
|
|
53
121
|
maxTokens?: number;
|
|
54
|
-
previousResponseId?: string;
|
|
55
122
|
}
|
|
56
123
|
|
|
57
124
|
export interface CompletionResult {
|
|
58
|
-
|
|
59
|
-
|
|
125
|
+
/**
|
|
126
|
+
* Ordered output blocks from the model — same shape as `AssistantTurn.blocks`.
|
|
127
|
+
* Caller can persist this directly as a new AssistantTurn in history.
|
|
128
|
+
*/
|
|
129
|
+
blocks: AssistantContentBlock[];
|
|
60
130
|
usage: TokenUsage;
|
|
61
131
|
finishReason: FinishReason;
|
|
132
|
+
/**
|
|
133
|
+
* Provider-issued response ID. Set by adapters that support server-side
|
|
134
|
+
* continuity (OpenAI). Caller persists this on the resulting AssistantTurn
|
|
135
|
+
* and passes it back as `Conversation.continuation.previousResponseId` next
|
|
136
|
+
* time.
|
|
137
|
+
*/
|
|
62
138
|
responseId?: string;
|
|
63
139
|
}
|
|
64
140
|
|
|
@@ -87,11 +163,20 @@ export interface StreamChunk {
|
|
|
87
163
|
export interface LLMProvider {
|
|
88
164
|
name: ProviderName;
|
|
89
165
|
models: string[];
|
|
166
|
+
/**
|
|
167
|
+
* Capability flag. When `true`, the listener may send
|
|
168
|
+
* `Conversation.mode = "continuation"` with a `previousResponseId` to skip
|
|
169
|
+
* resending history (OpenAI Responses API). When `false`, the listener must
|
|
170
|
+
* always send `Conversation.mode = "full"` with the full conversation
|
|
171
|
+
* history (Claude, Gemini).
|
|
172
|
+
*/
|
|
173
|
+
supportsContinuation: boolean;
|
|
90
174
|
complete(request: CompletionRequest): Promise<CompletionResult>;
|
|
91
175
|
streamComplete(
|
|
92
176
|
request: CompletionRequest,
|
|
93
177
|
onChunk: (chunk: StreamChunk) => void,
|
|
94
178
|
): Promise<CompletionResult>;
|
|
179
|
+
getPricing?(model: string): import("./billing/types").AdapterPricing | undefined;
|
|
95
180
|
}
|
|
96
181
|
|
|
97
182
|
// ─── Chat Stream (SSE events for chat streaming) ────────────────
|
|
@@ -100,7 +185,7 @@ export type ChatStreamEventType =
|
|
|
100
185
|
| "content_delta"
|
|
101
186
|
| "server_tool_start"
|
|
102
187
|
| "server_tool_result"
|
|
103
|
-
| "
|
|
188
|
+
| "interactive_tool_request"
|
|
104
189
|
| "usage_update"
|
|
105
190
|
| "done"
|
|
106
191
|
| "error";
|
|
@@ -118,11 +203,3 @@ export interface ChatStreamEvent {
|
|
|
118
203
|
error?: string;
|
|
119
204
|
}
|
|
120
205
|
|
|
121
|
-
// ─── Pricing ─────────────────────────────────────────────────────
|
|
122
|
-
|
|
123
|
-
export interface PricingConfig {
|
|
124
|
-
inputPer1M: number;
|
|
125
|
-
outputPer1M: number;
|
|
126
|
-
cachedInputPer1M?: number;
|
|
127
|
-
reasoningPer1M?: number;
|
|
128
|
-
}
|
package/src/aggregates/budget.ts
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/// <reference path="../arc.d.ts" />
|
|
2
|
-
import {
|
|
3
|
-
aggregate,
|
|
4
|
-
boolean,
|
|
5
|
-
id,
|
|
6
|
-
number,
|
|
7
|
-
string,
|
|
8
|
-
type ArcId,
|
|
9
|
-
} from "@arcote.tech/arc";
|
|
10
|
-
import type { Token } from "@arcote.tech/arc-auth";
|
|
11
|
-
import type { createCompletionAggregate } from "./completion";
|
|
12
|
-
|
|
13
|
-
// ─── ID ──────────────────────────────────────────────────────────
|
|
14
|
-
|
|
15
|
-
export const createBudgetId = <const Name extends string>(data: {
|
|
16
|
-
name: Name;
|
|
17
|
-
}) => id(`${data.name}Budget`);
|
|
18
|
-
|
|
19
|
-
export type BudgetId<Name extends string = string> = ReturnType<
|
|
20
|
-
typeof createBudgetId<Name>
|
|
21
|
-
>;
|
|
22
|
-
|
|
23
|
-
// ─── Aggregate ───────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
export type BudgetAggregateData = {
|
|
26
|
-
name: string;
|
|
27
|
-
budgetId: ArcId<any>;
|
|
28
|
-
accountId: ArcId<any>;
|
|
29
|
-
userToken: Token;
|
|
30
|
-
CompletionAggregate: ReturnType<typeof createCompletionAggregate>;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const createBudgetAggregate = <
|
|
34
|
-
const Data extends BudgetAggregateData,
|
|
35
|
-
>(
|
|
36
|
-
data: Data,
|
|
37
|
-
) => {
|
|
38
|
-
const { budgetId, accountId, userToken, CompletionAggregate } = data;
|
|
39
|
-
|
|
40
|
-
const completionFinishedEvent =
|
|
41
|
-
CompletionAggregate.getEvent("completionFinished");
|
|
42
|
-
|
|
43
|
-
return aggregate(`${data.name}Budgets`, budgetId, {
|
|
44
|
-
accountId,
|
|
45
|
-
budgetType: string(),
|
|
46
|
-
limitCents: number(),
|
|
47
|
-
usedCents: number(),
|
|
48
|
-
period: string().optional(),
|
|
49
|
-
isExceeded: boolean(),
|
|
50
|
-
})
|
|
51
|
-
.publicEvent(
|
|
52
|
-
"budgetSet",
|
|
53
|
-
{
|
|
54
|
-
budgetId,
|
|
55
|
-
accountId,
|
|
56
|
-
budgetType: string(),
|
|
57
|
-
limitCents: number(),
|
|
58
|
-
period: string().optional(),
|
|
59
|
-
},
|
|
60
|
-
async (ctx, event) => {
|
|
61
|
-
const p = event.payload;
|
|
62
|
-
await ctx.set(p.budgetId, {
|
|
63
|
-
accountId: p.accountId,
|
|
64
|
-
budgetType: p.budgetType,
|
|
65
|
-
limitCents: p.limitCents,
|
|
66
|
-
usedCents: 0,
|
|
67
|
-
period: p.period,
|
|
68
|
-
isExceeded: false,
|
|
69
|
-
});
|
|
70
|
-
},
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
.publicEvent(
|
|
74
|
-
"budgetUsageRecorded",
|
|
75
|
-
{
|
|
76
|
-
budgetId,
|
|
77
|
-
amountCents: number(),
|
|
78
|
-
},
|
|
79
|
-
async (ctx, event) => {
|
|
80
|
-
const existing = await ctx.findOne({ _id: event.payload.budgetId });
|
|
81
|
-
if (!existing) return;
|
|
82
|
-
|
|
83
|
-
const newUsed = existing.usedCents + event.payload.amountCents;
|
|
84
|
-
await ctx.modify(event.payload.budgetId, {
|
|
85
|
-
usedCents: newUsed,
|
|
86
|
-
isExceeded: newUsed >= existing.limitCents,
|
|
87
|
-
});
|
|
88
|
-
},
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
.handleEvent(completionFinishedEvent, async (ctx, event) => {
|
|
92
|
-
// Record usage from completion cost on all matching budgets
|
|
93
|
-
const budgets = await ctx.find({});
|
|
94
|
-
for (const budget of budgets) {
|
|
95
|
-
const newUsed = budget.usedCents + event.payload.costCents;
|
|
96
|
-
await ctx.modify(budget._id, {
|
|
97
|
-
usedCents: newUsed,
|
|
98
|
-
isExceeded: newUsed >= budget.limitCents,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
.mutateMethod(
|
|
104
|
-
"setBudget",
|
|
105
|
-
(fn) => fn.withParams({
|
|
106
|
-
budgetType: string(),
|
|
107
|
-
limitCents: number(),
|
|
108
|
-
period: string().optional(),
|
|
109
|
-
}).handle(
|
|
110
|
-
ONLY_SERVER &&
|
|
111
|
-
(async (ctx, params) => {
|
|
112
|
-
const bId = budgetId.generate();
|
|
113
|
-
const aId = ctx.$auth.params.accountId;
|
|
114
|
-
|
|
115
|
-
await ctx.budgetSet.emit({
|
|
116
|
-
budgetId: bId,
|
|
117
|
-
accountId: accountId.parse(aId),
|
|
118
|
-
budgetType: params.budgetType,
|
|
119
|
-
limitCents: params.limitCents,
|
|
120
|
-
period: params.period,
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
return { budgetId: bId };
|
|
124
|
-
}),
|
|
125
|
-
))
|
|
126
|
-
|
|
127
|
-
.clientQuery(
|
|
128
|
-
"getByAccount",
|
|
129
|
-
(fn) => fn.handle(async (ctx) =>
|
|
130
|
-
ctx.$query.find({ where: { accountId: ctx.$auth.params.accountId } }),
|
|
131
|
-
))
|
|
132
|
-
|
|
133
|
-
.protectBy(userToken, (p) => ({ accountId: p.accountId }))
|
|
134
|
-
;
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
export type BudgetAggregate = ReturnType<typeof createBudgetAggregate>;
|