@homeflare/alchemy 0.41.0 → 0.43.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/dist/litellm/budget-errors.d.ts +36 -0
- package/dist/litellm/budget-errors.d.ts.map +1 -0
- package/dist/litellm/budget-form.d.ts +50 -0
- package/dist/litellm/budget-form.d.ts.map +1 -0
- package/dist/litellm/budget-operations.d.ts +20 -0
- package/dist/litellm/budget-operations.d.ts.map +1 -0
- package/dist/litellm/budget.d.ts +61 -0
- package/dist/litellm/budget.d.ts.map +1 -0
- package/dist/litellm/fake-litellm.d.ts +5 -0
- package/dist/litellm/fake-litellm.d.ts.map +1 -1
- package/dist/litellm/index.d.ts +2 -0
- package/dist/litellm/index.d.ts.map +1 -1
- package/dist/litellm/index.js +125 -2
- package/dist/litellm/index.js.map +9 -5
- package/dist/litellm/operations.d.ts +22 -0
- package/dist/litellm/operations.d.ts.map +1 -1
- package/dist/litellm/providers.d.ts +1 -1
- package/dist/litellm/providers.d.ts.map +1 -1
- package/dist/openbao/auth-role-form.d.ts +9 -0
- package/dist/openbao/auth-role-form.d.ts.map +1 -1
- package/dist/openbao/auth-role-wire.d.ts.map +1 -1
- package/dist/openbao/fake-engines-roles.d.ts.map +1 -1
- package/dist/openbao/index.js +9 -6
- package/dist/openbao/index.js.map +4 -4
- package/dist/postgres/connection.d.ts +20 -2
- package/dist/postgres/connection.d.ts.map +1 -1
- package/dist/postgres/database-sql.d.ts +1 -1
- package/dist/postgres/database-sql.d.ts.map +1 -1
- package/dist/postgres/database.d.ts +1 -1
- package/dist/postgres/database.d.ts.map +1 -1
- package/dist/postgres/index.d.ts +5 -3
- package/dist/postgres/index.d.ts.map +1 -1
- package/dist/postgres/index.js +124 -38
- package/dist/postgres/index.js.map +8 -7
- package/dist/postgres/providers.d.ts +4 -2
- package/dist/postgres/providers.d.ts.map +1 -1
- package/dist/postgres/psql-executor.d.ts +21 -0
- package/dist/postgres/psql-executor.d.ts.map +1 -0
- package/dist/proxmox/acl.d.ts.map +1 -1
- package/dist/proxmox/api-token.d.ts.map +1 -1
- package/dist/proxmox/ceph-flag-types.d.ts +57 -0
- package/dist/proxmox/ceph-flag-types.d.ts.map +1 -0
- package/dist/proxmox/ceph-flag-wire.d.ts +77 -0
- package/dist/proxmox/ceph-flag-wire.d.ts.map +1 -0
- package/dist/proxmox/ceph-flag.d.ts +15 -50
- package/dist/proxmox/ceph-flag.d.ts.map +1 -1
- package/dist/proxmox/ceph-osd-write.d.ts +10 -13
- package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
- package/dist/proxmox/credential-errors.d.ts +18 -4
- package/dist/proxmox/credential-errors.d.ts.map +1 -1
- package/dist/proxmox/credentials.d.ts +35 -1
- package/dist/proxmox/credentials.d.ts.map +1 -1
- package/dist/proxmox/group.d.ts.map +1 -1
- package/dist/proxmox/index.js +805 -757
- package/dist/proxmox/index.js.map +21 -19
- package/dist/proxmox/lease-cache.d.ts +67 -5
- package/dist/proxmox/lease-cache.d.ts.map +1 -1
- package/dist/proxmox/mint.d.ts +4 -2
- package/dist/proxmox/mint.d.ts.map +1 -1
- package/dist/proxmox/node-network.d.ts.map +1 -1
- package/dist/proxmox/role.d.ts.map +1 -1
- package/dist/proxmox/storage.d.ts.map +1 -1
- package/dist/proxmox/unreadable-read.d.ts +8 -1
- package/dist/proxmox/unreadable-read.d.ts.map +1 -1
- package/dist/proxmox/user.d.ts.map +1 -1
- package/dist/proxmox/zfs-pool.d.ts.map +1 -1
- package/docs/credentials.md +14 -1
- package/docs/litellm-budget.md +55 -0
- package/docs/litellm.md +7 -6
- package/docs/postgres.md +22 -0
- package/docs/proxmox-sdk-migration.md +45 -0
- package/package.json +3 -3
- package/src/litellm/budget-errors.ts +38 -0
- package/src/litellm/budget-form.ts +94 -0
- package/src/litellm/budget-operations.ts +51 -0
- package/src/litellm/budget.test.ts +105 -0
- package/src/litellm/budget.ts +127 -0
- package/src/litellm/fake-litellm.ts +50 -1
- package/src/litellm/index.ts +13 -0
- package/src/litellm/operations.ts +1 -1
- package/src/litellm/providers.ts +4 -1
- package/src/openbao/auth-role-form.test.ts +37 -0
- package/src/openbao/auth-role-form.ts +12 -1
- package/src/openbao/auth-role-wire.test.ts +1 -0
- package/src/openbao/auth-role-wire.ts +13 -1
- package/src/openbao/distilled-retry.test.ts +1 -0
- package/src/openbao/fake-engines-roles.ts +7 -1
- package/src/postgres/connection.ts +37 -3
- package/src/postgres/database-sql.ts +4 -3
- package/src/postgres/database.ts +3 -2
- package/src/postgres/index.ts +10 -3
- package/src/postgres/providers.ts +15 -2
- package/src/postgres/psql-executor.test.ts +106 -0
- package/src/postgres/psql-executor.ts +130 -0
- package/src/proxmox/acl.ts +7 -1
- package/src/proxmox/api-token.ts +7 -1
- package/src/proxmox/ceph-flag-types.ts +70 -0
- package/src/proxmox/ceph-flag-wire.test.ts +72 -0
- package/src/proxmox/ceph-flag-wire.ts +123 -0
- package/src/proxmox/ceph-flag.ts +77 -130
- package/src/proxmox/ceph-osd-write.ts +10 -13
- package/src/proxmox/credential-errors.ts +19 -5
- package/src/proxmox/credentials.test.ts +73 -1
- package/src/proxmox/credentials.ts +72 -1
- package/src/proxmox/group.ts +7 -1
- package/src/proxmox/lease-cache-mint.test.ts +70 -0
- package/src/proxmox/lease-cache.test.ts +83 -7
- package/src/proxmox/lease-cache.ts +74 -10
- package/src/proxmox/mint.ts +12 -5
- package/src/proxmox/node-network.ts +7 -1
- package/src/proxmox/role.ts +7 -1
- package/src/proxmox/storage.ts +7 -1
- package/src/proxmox/unreadable-read.ts +16 -4
- package/src/proxmox/user.ts +7 -1
- package/src/proxmox/zfs-pool.ts +7 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LiteLLM.Budget` through Alchemy's real Plan and Apply over the fake proxy.
|
|
3
|
+
*/
|
|
4
|
+
import { expect, test } from 'bun:test';
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import { FAKE_BASE, startFakeLitellm } from './fake-litellm.ts';
|
|
7
|
+
import { fakeStack, writesOf } from './fake-stack.ts';
|
|
8
|
+
import { type BudgetProps, LiteLLMBudget } from './budget.ts';
|
|
9
|
+
import { differing, isHardCapUnjustified, toAttributes } from './budget-form.ts';
|
|
10
|
+
|
|
11
|
+
const KEY = 'sk-test-master';
|
|
12
|
+
const stack = (fake: ReturnType<typeof startFakeLitellm>) =>
|
|
13
|
+
fakeStack({ apiKey: KEY, baseUrl: FAKE_BASE }, fake.fetch);
|
|
14
|
+
const declare = (props: BudgetProps, name = 'Tier') =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
yield* LiteLLMBudget(name, props);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const tier = {
|
|
20
|
+
budget_id: 'hf-tier-agent',
|
|
21
|
+
budget_duration: '1d',
|
|
22
|
+
created_at: 'x',
|
|
23
|
+
max_budget: 2,
|
|
24
|
+
rpm_limit: '60',
|
|
25
|
+
soft_budget: null,
|
|
26
|
+
tpm_limit: null,
|
|
27
|
+
updated_at: 'x',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
test('creates a soft-only budget and sends no max_budget', async () => {
|
|
31
|
+
const fake = startFakeLitellm({ masterKey: KEY });
|
|
32
|
+
const planned = await stack(fake).deploy(
|
|
33
|
+
declare({ budgetId: 'hf-tier-x', budgetDuration: '1d', softBudget: 50 }),
|
|
34
|
+
);
|
|
35
|
+
expect(planned).toEqual({ Tier: 'create' });
|
|
36
|
+
expect(fake.budgets()[0]).toMatchObject({ budget_id: 'hf-tier-x', soft_budget: 50 });
|
|
37
|
+
expect(fake.budgets()[0]).not.toHaveProperty('max_budget');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('a second deploy of the same declaration writes nothing', async () => {
|
|
41
|
+
const fake = startFakeLitellm({ masterKey: KEY });
|
|
42
|
+
const props = { budgetId: 'hf-tier-x', softBudget: 50, rpmLimit: 60 };
|
|
43
|
+
const same = stack(fake);
|
|
44
|
+
await same.deploy(declare(props));
|
|
45
|
+
const before = fake.requests().length;
|
|
46
|
+
expect(await same.deploy(declare(props))).toEqual({ Tier: 'noop' });
|
|
47
|
+
expect(writesOf(fake.requests().slice(before))).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('a hard cap without a reason is refused before any write', async () => {
|
|
51
|
+
const fake = startFakeLitellm({ masterKey: KEY });
|
|
52
|
+
await expect(
|
|
53
|
+
stack(fake).deploy(declare({ budgetId: 'hf-tier-x', maxBudget: 60 })),
|
|
54
|
+
).rejects.toThrow();
|
|
55
|
+
expect(writesOf(fake.requests())).toEqual([]);
|
|
56
|
+
expect(isHardCapUnjustified({ maxBudget: 1, maxBudgetReason: ' ' })).toBe(true);
|
|
57
|
+
expect(isHardCapUnjustified({ maxBudget: 1, maxBudgetReason: 'abuse guard' })).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('a hard cap with a reason is written', async () => {
|
|
61
|
+
const fake = startFakeLitellm({ masterKey: KEY });
|
|
62
|
+
await stack(fake).deploy(
|
|
63
|
+
declare({ budgetId: 'hf-tier-x', maxBudget: 5, maxBudgetReason: 'public voice endpoint' }),
|
|
64
|
+
);
|
|
65
|
+
expect(fake.budgets()[0]).toMatchObject({ max_budget: 5 });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('a live row is Unowned: refused without --adopt, taken with it', async () => {
|
|
69
|
+
const fake = startFakeLitellm({ masterKey: KEY, budgetSeed: [tier] });
|
|
70
|
+
const props = {
|
|
71
|
+
budgetId: 'hf-tier-agent',
|
|
72
|
+
budgetDuration: '1d',
|
|
73
|
+
maxBudget: 2,
|
|
74
|
+
maxBudgetReason: 'measured live',
|
|
75
|
+
};
|
|
76
|
+
const engine = stack(fake);
|
|
77
|
+
await expect(engine.deploy(declare(props))).rejects.toThrow();
|
|
78
|
+
expect(writesOf(fake.requests())).toEqual([]);
|
|
79
|
+
const planned = await engine.deploy(declare({ ...props, rpmLimit: 60 }), { adopt: true });
|
|
80
|
+
expect(Object.values(planned)).toEqual(['adopted']);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('adopting a hard tier and declaring it soft clears max_budget', async () => {
|
|
84
|
+
const fake = startFakeLitellm({ masterKey: KEY, budgetSeed: [tier] });
|
|
85
|
+
await stack(fake).deploy(
|
|
86
|
+
declare({ budgetId: 'hf-tier-agent', budgetDuration: '1d', rpmLimit: 60, softBudget: 2 }),
|
|
87
|
+
{ adopt: true },
|
|
88
|
+
);
|
|
89
|
+
expect(fake.budgets()[0]).toMatchObject({ max_budget: null, soft_budget: 2 });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('a proxy that cannot clear a limit fails the apply instead of claiming success', async () => {
|
|
93
|
+
const fake = startFakeLitellm({ masterKey: KEY, budgetSeed: [tier], budgetExcludeNone: true });
|
|
94
|
+
await expect(
|
|
95
|
+
stack(fake).deploy(declare({ budgetId: 'hf-tier-agent', budgetDuration: '1d', rpmLimit: 60 }), {
|
|
96
|
+
adopt: true,
|
|
97
|
+
}),
|
|
98
|
+
).rejects.toThrow();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('form helpers: string limits become numbers, and differences are named', () => {
|
|
102
|
+
const live = toAttributes(tier);
|
|
103
|
+
expect(live.rpmLimit).toBe(60);
|
|
104
|
+
expect(differing(live, { budgetDuration: '1d', rpmLimit: 60 })).toEqual(['max_budget']);
|
|
105
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `LiteLLM.Budget` — one row of LiteLLM's `LiteLLM_BudgetTable` (`/budget/*`), the "tier" keys and
|
|
3
|
+
* teams bind to (`hf-tier-*`).
|
|
4
|
+
*
|
|
5
|
+
* ★ MONITORING, NOT LIMITS (Tim, 2026-09-28): `softBudget` is the prop; `maxBudget` is a hard cap
|
|
6
|
+
* that 429s every caller and is refused without `maxBudgetReason` (budget-form.ts).
|
|
7
|
+
* ★ ADOPT BY id. `budgetId` names the live row; without it a deterministic physical name is used.
|
|
8
|
+
* A foreign row with that id is `Unowned`, so it needs `--adopt`.
|
|
9
|
+
* ★ `defaultRemovalPolicy: 'retain'` — deleting a budget row that keys still reference either
|
|
10
|
+
* fails or detaches them, and a detached key loses its rate limits. Opt in with
|
|
11
|
+
* `.pipe(RemovalPolicy.destroy())`.
|
|
12
|
+
* ⛔ CLEARING A LIMIT: sent as an explicit `null`. If this LiteLLM merges with `exclude_none` the
|
|
13
|
+
* row keeps the old value; reconcile reads back and FAILS (`LitellmBudgetFieldNotClearedError`)
|
|
14
|
+
* rather than report a cleared cap. Unmeasured against a live proxy at 1.103.0.
|
|
15
|
+
* ⛔ NO CREDENTIAL IS A PROP; `litellmProviders`' layer supplies `Credentials`.
|
|
16
|
+
*/
|
|
17
|
+
import { Resource } from 'alchemy';
|
|
18
|
+
import { Unowned } from 'alchemy/AdoptPolicy';
|
|
19
|
+
import { isResolved } from 'alchemy/Diff';
|
|
20
|
+
import type { Input } from 'alchemy/Input';
|
|
21
|
+
import { createPhysicalName } from 'alchemy/PhysicalName';
|
|
22
|
+
import * as Provider from 'alchemy/Provider';
|
|
23
|
+
import * as Effect from 'effect/Effect';
|
|
24
|
+
import * as Predicate from 'effect/Predicate';
|
|
25
|
+
import {
|
|
26
|
+
LitellmBudgetFieldNotClearedError,
|
|
27
|
+
LitellmBudgetHardCapUnjustifiedError,
|
|
28
|
+
} from './budget-errors.ts';
|
|
29
|
+
import {
|
|
30
|
+
type BudgetAttributes,
|
|
31
|
+
type BudgetProps,
|
|
32
|
+
createBody,
|
|
33
|
+
differing,
|
|
34
|
+
isHardCapUnjustified,
|
|
35
|
+
updateBody,
|
|
36
|
+
} from './budget-form.ts';
|
|
37
|
+
import { createBudget, deleteBudget, listBudgets, updateBudget } from './budget-operations.ts';
|
|
38
|
+
|
|
39
|
+
export type { BudgetAttributes, BudgetProps };
|
|
40
|
+
export type { BudgetError } from './budget-errors.ts';
|
|
41
|
+
|
|
42
|
+
export interface LiteLLMBudget extends Resource<'LiteLLM.Budget', BudgetProps, BudgetAttributes> {}
|
|
43
|
+
|
|
44
|
+
export const LiteLLMBudget = Resource<LiteLLMBudget>('LiteLLM.Budget', {
|
|
45
|
+
defaultRemovalPolicy: 'retain',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const isLiteLLMBudget = (value: unknown): value is LiteLLMBudget =>
|
|
49
|
+
Predicate.hasProperty(value, 'Type') && value.Type === 'LiteLLM.Budget';
|
|
50
|
+
|
|
51
|
+
const refuseUnjustifiedCap = (props: BudgetProps) =>
|
|
52
|
+
isHardCapUnjustified(props)
|
|
53
|
+
? Effect.fail(new LitellmBudgetHardCapUnjustifiedError({ budgetId: props.budgetId }))
|
|
54
|
+
: Effect.void;
|
|
55
|
+
|
|
56
|
+
const idOf = (
|
|
57
|
+
id: string,
|
|
58
|
+
instanceId: string,
|
|
59
|
+
props: BudgetProps,
|
|
60
|
+
output: BudgetAttributes | undefined,
|
|
61
|
+
) =>
|
|
62
|
+
output?.budgetId !== undefined
|
|
63
|
+
? Effect.succeed(output.budgetId)
|
|
64
|
+
: props.budgetId !== undefined
|
|
65
|
+
? Effect.succeed(props.budgetId)
|
|
66
|
+
: createPhysicalName({ id, instanceId, lowercase: true, maxLength: 64 });
|
|
67
|
+
|
|
68
|
+
type Args<P> = {
|
|
69
|
+
id: string;
|
|
70
|
+
instanceId: string;
|
|
71
|
+
output: BudgetAttributes | undefined;
|
|
72
|
+
} & P;
|
|
73
|
+
|
|
74
|
+
/** ⛔ TEST-ONLY EXPORT. Not on index.ts's barrel. */
|
|
75
|
+
export const budgetHandlers = {
|
|
76
|
+
read: ({ id, instanceId, olds, output }: Args<{ olds: BudgetProps }>) =>
|
|
77
|
+
Effect.gen(function* () {
|
|
78
|
+
const budgetId = yield* idOf(id, instanceId, olds, output);
|
|
79
|
+
const live = (yield* listBudgets()).find((row) => row.budgetId === budgetId);
|
|
80
|
+
if (live === undefined) return undefined;
|
|
81
|
+
return output === undefined ? Unowned(live) : live;
|
|
82
|
+
}),
|
|
83
|
+
|
|
84
|
+
diff: ({ news, output }: { news: Input<BudgetProps>; output: BudgetAttributes | undefined }) =>
|
|
85
|
+
Effect.gen(function* () {
|
|
86
|
+
if (!isResolved(news)) return undefined;
|
|
87
|
+
yield* refuseUnjustifiedCap(news);
|
|
88
|
+
if (output === undefined) return undefined;
|
|
89
|
+
// The id is identity: a different declared id is a different row.
|
|
90
|
+
if (news.budgetId !== undefined && news.budgetId !== output.budgetId) {
|
|
91
|
+
return { action: 'replace', deleteFirst: false } as const;
|
|
92
|
+
}
|
|
93
|
+
return differing(output, news).length === 0
|
|
94
|
+
? ({ action: 'noop' } as const)
|
|
95
|
+
: ({ action: 'update' } as const);
|
|
96
|
+
}),
|
|
97
|
+
|
|
98
|
+
reconcile: ({ id, instanceId, news, output }: Args<{ news: BudgetProps }>) =>
|
|
99
|
+
Effect.gen(function* () {
|
|
100
|
+
yield* refuseUnjustifiedCap(news);
|
|
101
|
+
const budgetId = yield* idOf(id, instanceId, news, output);
|
|
102
|
+
const before = (yield* listBudgets()).find((row) => row.budgetId === budgetId);
|
|
103
|
+
if (before === undefined) yield* createBudget(createBody(news, budgetId));
|
|
104
|
+
else if (differing(before, news).length > 0) yield* updateBudget(updateBody(news, before));
|
|
105
|
+
|
|
106
|
+
const after = (yield* listBudgets()).find((row) => row.budgetId === budgetId);
|
|
107
|
+
if (after === undefined) {
|
|
108
|
+
return yield* Effect.die(
|
|
109
|
+
new Error(`LiteLLM.Budget ${budgetId}: write returned no error but the row is absent.`),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const left = differing(after, news);
|
|
113
|
+
if (left.length > 0) {
|
|
114
|
+
return yield* Effect.fail(
|
|
115
|
+
new LitellmBudgetFieldNotClearedError({ budgetId, fields: left }),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return after;
|
|
119
|
+
}),
|
|
120
|
+
|
|
121
|
+
delete: ({ output }: { output: BudgetAttributes }) => deleteBudget(output.budgetId),
|
|
122
|
+
|
|
123
|
+
list: () => Effect.succeed([]),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const LiteLLMBudgetProvider = () =>
|
|
127
|
+
Provider.effect(LiteLLMBudget, Effect.succeed(LiteLLMBudget.Provider.of(budgetHandlers)));
|
|
@@ -40,6 +40,8 @@ export interface FakeLitellm {
|
|
|
40
40
|
readonly fetch: typeof globalThis.fetch;
|
|
41
41
|
readonly rows: () => readonly PassThroughGenericEndpoint[];
|
|
42
42
|
readonly requests: () => readonly FakeRequest[];
|
|
43
|
+
/** `/budget/*` rows, wire-shaped (`tpm_limit`/`rpm_limit` as decimal strings, like the real table). */
|
|
44
|
+
readonly budgets: () => readonly Record<string, unknown>[];
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
const RACE_WINDOW_MS = 15;
|
|
@@ -53,11 +55,52 @@ export const startFakeLitellm = (options?: {
|
|
|
53
55
|
readonly seed?: readonly PassThroughGenericEndpoint[];
|
|
54
56
|
/** Every `DELETE` 400s `not_allowed_access`, whether or not the row exists — see the file header. */
|
|
55
57
|
readonly forbidDelete?: boolean;
|
|
58
|
+
readonly budgetSeed?: readonly Record<string, unknown>[];
|
|
59
|
+
/** `/budget/update` drops `null`s instead of writing them (an `exclude_none` merge). */
|
|
60
|
+
readonly budgetExcludeNone?: boolean;
|
|
56
61
|
}): FakeLitellm => {
|
|
57
62
|
const masterKey = options?.masterKey ?? 'sk-test-master';
|
|
58
63
|
const forbidDelete = options?.forbidDelete ?? false;
|
|
59
64
|
let rows: PassThroughGenericEndpoint[] = [...(options?.seed ?? [])];
|
|
60
65
|
const requests: FakeRequest[] = [];
|
|
66
|
+
let budgets: Record<string, unknown>[] = [...(options?.budgetSeed ?? [])];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* ⚠️ Shapes from the generated 1.100.0 schema (`budget_management.ts`), not a live 1.103.0 read.
|
|
70
|
+
* Create of an existing id is 400 (a unique-key failure); update/delete of a missing id is 400
|
|
71
|
+
* (unmeasured, the SDK patch for these routes declares `BadRequest`).
|
|
72
|
+
*/
|
|
73
|
+
const budgetRoute = async (request: Request, url: URL) => {
|
|
74
|
+
if (request.method === 'GET' && url.pathname === '/budget/list') return json(200, budgets);
|
|
75
|
+
const body = (await request.json()) as Record<string, unknown>;
|
|
76
|
+
const id = String(body['budget_id'] ?? body['id']);
|
|
77
|
+
const at = budgets.findIndex((row) => row['budget_id'] === id);
|
|
78
|
+
const wire = (fields: Record<string, unknown>) =>
|
|
79
|
+
Object.fromEntries(
|
|
80
|
+
Object.entries(fields).map(([k, v]) => [
|
|
81
|
+
k,
|
|
82
|
+
k.endsWith('_limit') && v !== null ? String(v) : v,
|
|
83
|
+
]),
|
|
84
|
+
);
|
|
85
|
+
if (url.pathname === '/budget/new') {
|
|
86
|
+
if (at !== -1) return json(400, { detail: `budget ${id} exists` });
|
|
87
|
+
budgets = [...budgets, { created_at: 'now', updated_at: 'now', ...wire(body) }];
|
|
88
|
+
return json(200, {});
|
|
89
|
+
}
|
|
90
|
+
if (at === -1) return json(400, { detail: `budget ${id} not found` });
|
|
91
|
+
if (url.pathname === '/budget/update') {
|
|
92
|
+
const patch = options?.budgetExcludeNone
|
|
93
|
+
? Object.fromEntries(Object.entries(body).filter(([, v]) => v !== null))
|
|
94
|
+
: body;
|
|
95
|
+
budgets = budgets.map((row, i) => (i === at ? { ...row, ...wire(patch) } : row));
|
|
96
|
+
return json(200, {});
|
|
97
|
+
}
|
|
98
|
+
if (url.pathname === '/budget/delete') {
|
|
99
|
+
budgets = budgets.filter((_, i) => i !== at);
|
|
100
|
+
return json(200, {});
|
|
101
|
+
}
|
|
102
|
+
return json(404, { detail: 'not found' });
|
|
103
|
+
};
|
|
61
104
|
|
|
62
105
|
const fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
63
106
|
const request =
|
|
@@ -66,6 +109,7 @@ export const startFakeLitellm = (options?: {
|
|
|
66
109
|
requests.push({ method: request.method, path: `${url.pathname}${url.search}` });
|
|
67
110
|
const auth = request.headers.get('authorization');
|
|
68
111
|
if (auth !== `Bearer ${masterKey}`) return json(401, { detail: 'invalid api key' });
|
|
112
|
+
if (url.pathname.startsWith('/budget/')) return budgetRoute(request, url);
|
|
69
113
|
if (
|
|
70
114
|
url.pathname !== '/config/pass_through_endpoint' &&
|
|
71
115
|
!url.pathname.startsWith('/config/pass_through_endpoint/')
|
|
@@ -117,5 +161,10 @@ export const startFakeLitellm = (options?: {
|
|
|
117
161
|
return json(405, { detail: `unhandled ${request.method} ${url.pathname}` });
|
|
118
162
|
}) as typeof globalThis.fetch;
|
|
119
163
|
|
|
120
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
budgets: () => [...budgets],
|
|
166
|
+
fetch,
|
|
167
|
+
requests: () => [...requests],
|
|
168
|
+
rows: () => [...rows],
|
|
169
|
+
};
|
|
121
170
|
};
|
package/src/litellm/index.ts
CHANGED
|
@@ -25,4 +25,17 @@ export {
|
|
|
25
25
|
type PassThroughEndpointError,
|
|
26
26
|
type PassThroughEndpointProps,
|
|
27
27
|
} from './pass-through-endpoint.ts';
|
|
28
|
+
export {
|
|
29
|
+
LiteLLMBudget,
|
|
30
|
+
LiteLLMBudgetProvider,
|
|
31
|
+
isLiteLLMBudget,
|
|
32
|
+
type BudgetAttributes,
|
|
33
|
+
type BudgetError,
|
|
34
|
+
type BudgetProps,
|
|
35
|
+
} from './budget.ts';
|
|
36
|
+
export {
|
|
37
|
+
LitellmBudgetFieldNotClearedError,
|
|
38
|
+
LitellmBudgetHardCapUnjustifiedError,
|
|
39
|
+
LitellmBudgetUnreadableError,
|
|
40
|
+
} from './budget-errors.ts';
|
|
28
41
|
export { litellmProviders } from './providers.ts';
|
|
@@ -77,7 +77,7 @@ const semaphoreFor = (baseUrl: string): Effect.Effect<Semaphore.Semaphore> =>
|
|
|
77
77
|
* `Fetch` service still wins inside that client, which is how the fake proxy stays the test
|
|
78
78
|
* transport.
|
|
79
79
|
*/
|
|
80
|
-
const throughFetch = <A, E, R>(io: Effect.Effect<A, E, R>) =>
|
|
80
|
+
export const throughFetch = <A, E, R>(io: Effect.Effect<A, E, R>) =>
|
|
81
81
|
io.pipe(Effect.provide(FetchHttpClient.layer));
|
|
82
82
|
|
|
83
83
|
const mutate = <A, E>(
|
package/src/litellm/providers.ts
CHANGED
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { type Credentials, CredentialsFromEnv } from '@distilled.cloud/litellm/Credentials';
|
|
22
22
|
import * as Layer from 'effect/Layer';
|
|
23
|
+
import { LiteLLMBudgetProvider } from './budget.ts';
|
|
23
24
|
import { LiteLLMPassThroughEndpointProvider } from './pass-through-endpoint.ts';
|
|
24
25
|
|
|
25
26
|
export const litellmProviders = (creds: Layer.Layer<Credentials> = CredentialsFromEnv) =>
|
|
26
|
-
LiteLLMPassThroughEndpointProvider().pipe(
|
|
27
|
+
Layer.mergeAll(LiteLLMPassThroughEndpointProvider(), LiteLLMBudgetProvider()).pipe(
|
|
28
|
+
Layer.provideMerge(creds),
|
|
29
|
+
);
|
|
@@ -43,3 +43,40 @@ describe('auth role secret_id TTL', () => {
|
|
|
43
43
|
expect(matches(attributesOf(PROPS, { ...live, secret_id_ttl: 0 }), PROPS)).toBe(false);
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
describe('auth role token_period', () => {
|
|
48
|
+
const LIVE = {
|
|
49
|
+
bind_secret_id: true,
|
|
50
|
+
secret_id_num_uses: 0,
|
|
51
|
+
secret_id_ttl: 7776000,
|
|
52
|
+
token_max_ttl: 3600,
|
|
53
|
+
token_period: 0,
|
|
54
|
+
token_policies: ['default', 'host-cert'],
|
|
55
|
+
token_ttl: 900,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
it('omits token_period from the SDK write when the prop is unset', async () => {
|
|
59
|
+
expect((await Effect.runPromise(authRoleRequest(PROPS))).token_period).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('writes 24h as the equivalent SDK seconds, never legacy period', async () => {
|
|
63
|
+
const props = { ...PROPS, tokenPeriod: '24h' };
|
|
64
|
+
const request = await Effect.runPromise(authRoleRequest(props));
|
|
65
|
+
expect(request.token_period).toBe(86400);
|
|
66
|
+
expect(request).not.toHaveProperty('period');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('reads a live token_period back and matches an equivalent declaration', () => {
|
|
70
|
+
const props = { ...PROPS, tokenPeriod: '24h' };
|
|
71
|
+
const attributes = attributesOf(props, { ...LIVE, token_period: 86400 });
|
|
72
|
+
expect(attributes.tokenPeriod).toBe('1d');
|
|
73
|
+
expect(matches(attributes, props)).toBe(true);
|
|
74
|
+
expect(matches(attributesOf(props, { ...LIVE, token_period: 3600 }), props)).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('leaves an unmanaged live token_period out of drift when the prop is unset', () => {
|
|
78
|
+
const attributes = attributesOf(PROPS, { ...LIVE, token_period: 86400 });
|
|
79
|
+
expect(attributes.tokenPeriod).toBe('1d');
|
|
80
|
+
expect(matches(attributes, PROPS)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -16,6 +16,14 @@ export interface BaoAuthRoleProps {
|
|
|
16
16
|
bindSecretId?: boolean;
|
|
17
17
|
/** `0` means unlimited uses — the shape reconcile.sh writes for headless roles. */
|
|
18
18
|
secretIdNumUses?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Token period, e.g. `24h` — set only for a periodic (forever-renewable) role, where
|
|
21
|
+
* `tokenTtl`/`tokenMaxTtl` are conventionally `'0'`. Omitted means this declaration does not
|
|
22
|
+
* manage the field: a live role's `token_period` (however it got set) is left untouched, the
|
|
23
|
+
* same way an omitted `bindSecretId` is. See house `openbao/approles.ts` for the role this
|
|
24
|
+
* unblocks — a periodic agent token that was inexpressible with these props.
|
|
25
|
+
*/
|
|
26
|
+
tokenPeriod?: string;
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
export interface BaoAuthRoleAttributes {
|
|
@@ -26,6 +34,7 @@ export interface BaoAuthRoleAttributes {
|
|
|
26
34
|
secretIdTtl: string;
|
|
27
35
|
bindSecretId: boolean;
|
|
28
36
|
secretIdNumUses: number;
|
|
37
|
+
tokenPeriod: string;
|
|
29
38
|
/** SHA-256 of the managed fields — safe to persist; see policy.ts. */
|
|
30
39
|
digest: string;
|
|
31
40
|
}
|
|
@@ -64,6 +73,7 @@ export const attributesOf = (
|
|
|
64
73
|
secretIdTtl: ttlText(ttlSeconds(live['secret_id_ttl']), '0'),
|
|
65
74
|
bindSecretId: bool(live['bind_secret_id'], true),
|
|
66
75
|
secretIdNumUses: int(live['secret_id_num_uses'], 0),
|
|
76
|
+
tokenPeriod: ttlText(ttlSeconds(live['token_period']), '0'),
|
|
67
77
|
};
|
|
68
78
|
return { ...attrs, digest: sha256(JSON.stringify(attrs)) };
|
|
69
79
|
};
|
|
@@ -88,6 +98,7 @@ export const matches = (attributes: BaoAuthRoleAttributes, props: BaoAuthRolePro
|
|
|
88
98
|
sameTtl(props.tokenMaxTtl, attributes.tokenMaxTtl) &&
|
|
89
99
|
sameTtl(props.secretIdTtl, attributes.secretIdTtl) &&
|
|
90
100
|
(props.bindSecretId === undefined || attributes.bindSecretId === props.bindSecretId) &&
|
|
91
|
-
(props.secretIdNumUses === undefined || attributes.secretIdNumUses === props.secretIdNumUses)
|
|
101
|
+
(props.secretIdNumUses === undefined || attributes.secretIdNumUses === props.secretIdNumUses) &&
|
|
102
|
+
(props.tokenPeriod === undefined || sameTtl(props.tokenPeriod, attributes.tokenPeriod))
|
|
92
103
|
);
|
|
93
104
|
};
|
|
@@ -35,6 +35,7 @@ const completeRole = (live: AppRoleReadRoleResponse): boolean =>
|
|
|
35
35
|
seconds(live.secret_id_ttl) &&
|
|
36
36
|
seconds(live.token_ttl) &&
|
|
37
37
|
seconds(live.token_max_ttl) &&
|
|
38
|
+
seconds(live.token_period) &&
|
|
38
39
|
Array.isArray(live.token_policies) &&
|
|
39
40
|
live.token_policies.every((policy) => typeof policy === 'string');
|
|
40
41
|
|
|
@@ -56,7 +57,14 @@ export const authRoleRequest = (props: BaoAuthRoleProps) =>
|
|
|
56
57
|
const tokenTtl = parseDuration(props.tokenTtl);
|
|
57
58
|
const tokenMaxTtl = parseDuration(props.tokenMaxTtl);
|
|
58
59
|
const secretIdTtl = parseDuration(props.secretIdTtl);
|
|
59
|
-
|
|
60
|
+
const tokenPeriod =
|
|
61
|
+
props.tokenPeriod === undefined ? undefined : parseDuration(props.tokenPeriod);
|
|
62
|
+
if (
|
|
63
|
+
!seconds(tokenTtl) ||
|
|
64
|
+
!seconds(tokenMaxTtl) ||
|
|
65
|
+
!seconds(secretIdTtl) ||
|
|
66
|
+
(props.tokenPeriod !== undefined && !seconds(tokenPeriod))
|
|
67
|
+
) {
|
|
60
68
|
return yield* Effect.fail(
|
|
61
69
|
new BaoError(0, 'POST', readPath(props.name), ['invalid role TTL duration']),
|
|
62
70
|
);
|
|
@@ -64,6 +72,9 @@ export const authRoleRequest = (props: BaoAuthRoleProps) =>
|
|
|
64
72
|
// ★ The former CLI-style body used strings for every k=v. The SDK schema uses seconds,
|
|
65
73
|
// booleans and arrays; omitted knobs stay omitted on update.
|
|
66
74
|
// pathRoleCreateUpdate preserves absent bind_secret_id/secret_id_num_uses on existing roles.
|
|
75
|
+
// ⛔ token_period, not period — the vendor doc on both fields says period is legacy and loses
|
|
76
|
+
// to token_period when both are set (approle.ts AppRoleWriteRoleRequest); this Resource only
|
|
77
|
+
// ever emits the modern field.
|
|
67
78
|
const request: AppRoleWriteRoleRequest = {
|
|
68
79
|
...identity(props.name),
|
|
69
80
|
token_policies: [...props.tokenPolicies],
|
|
@@ -72,6 +83,7 @@ export const authRoleRequest = (props: BaoAuthRoleProps) =>
|
|
|
72
83
|
secret_id_ttl: secretIdTtl,
|
|
73
84
|
...(props.bindSecretId === undefined ? {} : { bind_secret_id: props.bindSecretId }),
|
|
74
85
|
...(props.secretIdNumUses === undefined ? {} : { secret_id_num_uses: props.secretIdNumUses }),
|
|
86
|
+
...(tokenPeriod === undefined ? {} : { token_period: tokenPeriod }),
|
|
75
87
|
};
|
|
76
88
|
return request;
|
|
77
89
|
});
|
|
@@ -57,7 +57,13 @@ export const wireRoles = (): Store =>
|
|
|
57
57
|
store<Stored>(
|
|
58
58
|
(path, body, before) =>
|
|
59
59
|
path.startsWith('auth/approle/role/')
|
|
60
|
-
? {
|
|
60
|
+
? {
|
|
61
|
+
bind_secret_id: true,
|
|
62
|
+
secret_id_num_uses: 0,
|
|
63
|
+
token_period: 0,
|
|
64
|
+
...before,
|
|
65
|
+
...typed(body),
|
|
66
|
+
}
|
|
61
67
|
: typed(body),
|
|
62
68
|
(path) => keyOf(path),
|
|
63
69
|
);
|
|
@@ -18,6 +18,8 @@ import * as Context from 'effect/Context';
|
|
|
18
18
|
import * as Effect from 'effect/Effect';
|
|
19
19
|
import * as Layer from 'effect/Layer';
|
|
20
20
|
import type { SqlError } from 'effect/unstable/sql/SqlError';
|
|
21
|
+
import type { PgExecutor } from './database-sql.ts';
|
|
22
|
+
import { type PsqlRunner, makePsqlExecutor } from './psql-executor.ts';
|
|
21
23
|
|
|
22
24
|
/** What a stack passes to reach one cluster. `host` may be a socket directory (one beginning
|
|
23
25
|
* with `/` expands to `${host}/.s.PGSQL.${port}` — measured in
|
|
@@ -31,11 +33,27 @@ export interface PostgresConnectionConfig {
|
|
|
31
33
|
readonly ssl?: boolean;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/** The runner transport (`psql-executor.ts`): SQL goes through `run` instead of a socket, for a
|
|
37
|
+
* host whose Postgres is reachable only by a command (CT100: ssh + `podman exec`). `template`
|
|
38
|
+
* is the `CREATE DATABASE` template — `template0` by default, because CT100's `template1` is
|
|
39
|
+
* ParadeDB's and would copy postgis/pg_ivm/paradedb objects into every new database. */
|
|
40
|
+
export interface PostgresRunnerConfig {
|
|
41
|
+
readonly run: PsqlRunner;
|
|
42
|
+
readonly database: string;
|
|
43
|
+
readonly username: string;
|
|
44
|
+
readonly template?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** What `withPg`'s callback learns about the transport beyond the client. */
|
|
48
|
+
export interface PgContext {
|
|
49
|
+
readonly template?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
/** The lazy connection service. Its value is an `Effect` of the config, per S24 — see the file
|
|
35
53
|
* header for why. */
|
|
36
54
|
export class PostgresConnection extends Context.Service<
|
|
37
55
|
PostgresConnection,
|
|
38
|
-
Effect.Effect<PostgresConnectionConfig>
|
|
56
|
+
Effect.Effect<PostgresConnectionConfig | PostgresRunnerConfig>
|
|
39
57
|
>()('Postgres.Connection') {}
|
|
40
58
|
|
|
41
59
|
/** A stack's one-line way to provide a cluster: `Layer.provide(postgresConnection({ … }))` on
|
|
@@ -44,6 +62,14 @@ export const postgresConnection = (
|
|
|
44
62
|
config: PostgresConnectionConfig,
|
|
45
63
|
): Layer.Layer<PostgresConnection> => Layer.succeed(PostgresConnection, Effect.succeed(config));
|
|
46
64
|
|
|
65
|
+
/** A stack's one-line way to provide a cluster reached through a command runner. */
|
|
66
|
+
export const postgresRunnerConnection = (
|
|
67
|
+
config: PostgresRunnerConfig,
|
|
68
|
+
): Layer.Layer<PostgresConnection> => Layer.succeed(PostgresConnection, Effect.succeed(config));
|
|
69
|
+
|
|
70
|
+
const isRunner = (c: PostgresConnectionConfig | PostgresRunnerConfig): c is PostgresRunnerConfig =>
|
|
71
|
+
'run' in c;
|
|
72
|
+
|
|
47
73
|
/**
|
|
48
74
|
* Run one operation against a freshly built, freshly closed connection pool.
|
|
49
75
|
*
|
|
@@ -53,10 +79,18 @@ export const postgresConnection = (
|
|
|
53
79
|
* closes its own.
|
|
54
80
|
*/
|
|
55
81
|
export const withPg = <A, E>(
|
|
56
|
-
build: (pg: PgClient.PgClient) => Effect.Effect<A, E>,
|
|
82
|
+
build: (pg: PgClient.PgClient | PgExecutor, context: PgContext) => Effect.Effect<A, E>,
|
|
57
83
|
): Effect.Effect<A, E | SqlError, PostgresConnection> =>
|
|
58
84
|
Effect.gen(function* () {
|
|
59
85
|
const resolveConfig = yield* PostgresConnection;
|
|
60
86
|
const config = yield* resolveConfig;
|
|
61
|
-
|
|
87
|
+
if (isRunner(config)) {
|
|
88
|
+
return yield* build(makePsqlExecutor(config.run, config), {
|
|
89
|
+
template: config.template ?? 'template0',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return yield* Effect.provide(
|
|
93
|
+
Effect.flatMap(PgClient.PgClient, (pg) => build(pg, {})),
|
|
94
|
+
PgClient.layer(config),
|
|
95
|
+
);
|
|
62
96
|
});
|
|
@@ -47,8 +47,9 @@ export const quoteIdent = (value: string): string => `"${value.replace(/"/g, '""
|
|
|
47
47
|
* value that is locale or encoding text, never for an identifier (see the file header). */
|
|
48
48
|
export const quoteStringLiteral = (value: string): string => `'${value.replace(/'/g, "''")}'`;
|
|
49
49
|
|
|
50
|
-
const withOptions = (props: PostgresDatabaseProps): string => {
|
|
50
|
+
const withOptions = (props: PostgresDatabaseProps, template?: string): string => {
|
|
51
51
|
const parts: string[] = [`OWNER ${quoteIdent(props.owner)}`];
|
|
52
|
+
if (template !== undefined) parts.push(`TEMPLATE ${quoteIdent(template)}`);
|
|
52
53
|
if (props.encoding !== undefined) parts.push(`ENCODING ${quoteStringLiteral(props.encoding)}`);
|
|
53
54
|
if (props.localeProvider !== undefined) {
|
|
54
55
|
parts.push(`LOCALE_PROVIDER ${quoteStringLiteral(props.localeProvider)}`);
|
|
@@ -74,8 +75,8 @@ const withOptions = (props: PostgresDatabaseProps): string => {
|
|
|
74
75
|
* `Math.trunc`ed and stringified, and a boolean is one of exactly two literal words this file
|
|
75
76
|
* writes, so no declared value reaches the statement through string interpolation unescaped.
|
|
76
77
|
*/
|
|
77
|
-
export const buildCreateDatabaseSql = (props: PostgresDatabaseProps): string =>
|
|
78
|
-
`CREATE DATABASE ${quoteIdent(props.name)} WITH ${withOptions(props)}`;
|
|
78
|
+
export const buildCreateDatabaseSql = (props: PostgresDatabaseProps, template?: string): string =>
|
|
79
|
+
`CREATE DATABASE ${quoteIdent(props.name)} WITH ${withOptions(props, template)}`;
|
|
79
80
|
|
|
80
81
|
/** `42P04` (`duplicate_database`) is a race: something else created the name between our read
|
|
81
82
|
* and our create. Measured at `internal/sqlError.ts@effect/sql-pg 4.0.0-rc.115`: every SQLSTATE
|
package/src/postgres/database.ts
CHANGED
|
@@ -80,6 +80,7 @@ const liveDrift = (props: PostgresDatabaseProps, live: PostgresDatabaseAttribute
|
|
|
80
80
|
export const reconcileWithClient = (
|
|
81
81
|
pg: PgExecutor,
|
|
82
82
|
props: PostgresDatabaseProps,
|
|
83
|
+
template?: string,
|
|
83
84
|
): Effect.Effect<
|
|
84
85
|
PostgresDatabaseAttributes,
|
|
85
86
|
PostgresDatabaseOwnerMissing | PostgresDatabaseDrift | PostgresDatabaseCreateVanished | SqlError
|
|
@@ -93,7 +94,7 @@ export const reconcileWithClient = (
|
|
|
93
94
|
new PostgresDatabaseOwnerMissing({ database: props.name, owner: props.owner }),
|
|
94
95
|
);
|
|
95
96
|
}
|
|
96
|
-
yield* pg.unsafe(buildCreateDatabaseSql(props)).pipe(
|
|
97
|
+
yield* pg.unsafe(buildCreateDatabaseSql(props, template)).pipe(
|
|
97
98
|
Effect.asVoid,
|
|
98
99
|
Effect.catchTag('SqlError', (error) =>
|
|
99
100
|
isDuplicateDatabaseRace(error) ? Effect.succeed(undefined) : Effect.fail(error),
|
|
@@ -183,7 +184,7 @@ export const postgresDatabaseHandlers = PostgresDatabase.Provider.of({
|
|
|
183
184
|
new PostgresDatabaseNameRefused({ name: news.name, ...nameRefusal }),
|
|
184
185
|
);
|
|
185
186
|
}
|
|
186
|
-
return yield* withPg((pg) => reconcileWithClient(pg, news));
|
|
187
|
+
return yield* withPg((pg, ctx) => reconcileWithClient(pg, news, ctx.template));
|
|
187
188
|
}),
|
|
188
189
|
|
|
189
190
|
delete: ({ olds }) => Effect.fail(new PostgresDatabaseDropRefused({ database: olds.name })),
|
package/src/postgres/index.ts
CHANGED
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
* ⛔ THIS BARREL IS THE PUBLIC API, deliberately smaller than the directory: `fake-sql.ts` is a
|
|
6
6
|
* test double, not something a consuming stack should import.
|
|
7
7
|
*/
|
|
8
|
-
export type { PostgresConnectionConfig } from './connection.ts';
|
|
9
|
-
export {
|
|
8
|
+
export type { PostgresConnectionConfig, PostgresRunnerConfig } from './connection.ts';
|
|
9
|
+
export {
|
|
10
|
+
PostgresConnection,
|
|
11
|
+
postgresConnection,
|
|
12
|
+
postgresRunnerConnection,
|
|
13
|
+
withPg,
|
|
14
|
+
} from './connection.ts';
|
|
15
|
+
export type { PsqlResult, PsqlRunner } from './psql-executor.ts';
|
|
16
|
+
export { inlineParams, makePsqlExecutor } from './psql-executor.ts';
|
|
10
17
|
export type { PostgresDatabaseAttributes, PostgresDatabaseProps } from './database-attrs.ts';
|
|
11
18
|
export { POSTGRES_NAME_MAX_BYTES, nameByteRefusal, utf8ByteLength } from './database-attrs.ts';
|
|
12
19
|
export {
|
|
@@ -27,4 +34,4 @@ export {
|
|
|
27
34
|
PostgresDatabaseRenameRefused,
|
|
28
35
|
type PostgresDatabaseError,
|
|
29
36
|
} from './errors.ts';
|
|
30
|
-
export { postgresProviders } from './providers.ts';
|
|
37
|
+
export { postgresProviders, postgresRunnerProviders } from './providers.ts';
|