@homeflare/alchemy 0.28.0 → 0.29.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/README.md +2 -0
- package/dist/caddy/admin-address.d.ts +27 -0
- package/dist/caddy/admin-address.d.ts.map +1 -0
- package/dist/caddy/admin-calls.d.ts +25 -28
- package/dist/caddy/admin-calls.d.ts.map +1 -1
- package/dist/caddy/admin.d.ts +28 -38
- package/dist/caddy/admin.d.ts.map +1 -1
- package/dist/caddy/caddy-http-client.d.ts +25 -0
- package/dist/caddy/caddy-http-client.d.ts.map +1 -0
- package/dist/caddy/config-errors.d.ts +24 -0
- package/dist/caddy/config-errors.d.ts.map +1 -0
- package/dist/caddy/config-lifecycle.d.ts +20 -49
- package/dist/caddy/config-lifecycle.d.ts.map +1 -1
- package/dist/caddy/config-reconcile.d.ts +45 -0
- package/dist/caddy/config-reconcile.d.ts.map +1 -0
- package/dist/caddy/config.d.ts +4 -3
- package/dist/caddy/config.d.ts.map +1 -1
- package/dist/caddy/fake-caddy.d.ts +10 -2
- package/dist/caddy/fake-caddy.d.ts.map +1 -1
- package/dist/caddy/index.d.ts +3 -3
- package/dist/caddy/index.d.ts.map +1 -1
- package/dist/caddy/index.js +487 -444
- package/dist/caddy/index.js.map +16 -12
- package/dist/caddy/local-admin.d.ts +20 -34
- package/dist/caddy/local-admin.d.ts.map +1 -1
- package/dist/caddy/providers.d.ts +19 -3
- package/dist/caddy/providers.d.ts.map +1 -1
- package/dist/cloudflare/mesh-node-token.d.ts +1 -1
- package/dist/cloudflare/mesh-node-token.d.ts.map +1 -1
- package/dist/forgejo/branch-protection.d.ts +4 -4
- package/dist/forgejo/org-actions-secrets.d.ts +4 -4
- package/dist/forgejo/org-label.d.ts +4 -4
- package/dist/forgejo/org-team.d.ts +4 -4
- package/dist/forgejo/repo-webhook.d.ts +4 -4
- package/dist/forgejo/repository.d.ts +4 -4
- package/dist/forgejo/team-member.d.ts +4 -4
- package/dist/github/declare-repo-baseline.d.ts +1 -1
- package/dist/github/declare-repo-baseline.d.ts.map +1 -1
- package/dist/github/index.d.ts +8 -4
- package/dist/github/index.d.ts.map +1 -1
- package/dist/github/index.js +63 -23
- package/dist/github/index.js.map +13 -12
- package/dist/github/repo-baseline-data.d.ts +10 -1
- package/dist/github/repo-baseline-data.d.ts.map +1 -1
- package/dist/github/repo-policy.d.ts +1 -1
- package/dist/github/repo-policy.d.ts.map +1 -1
- package/dist/github/repository-ruleset-errors.d.ts +3 -2
- package/dist/github/repository-ruleset-errors.d.ts.map +1 -1
- package/dist/github/repository-ruleset-form.d.ts.map +1 -1
- package/dist/github/repository-ruleset-guards.d.ts +3 -2
- package/dist/github/repository-ruleset-guards.d.ts.map +1 -1
- package/dist/github/repository-ruleset-octokit.d.ts +49 -0
- package/dist/github/repository-ruleset-octokit.d.ts.map +1 -1
- package/dist/github/repository-ruleset-probe.d.ts +2 -1
- package/dist/github/repository-ruleset-probe.d.ts.map +1 -1
- package/dist/github/repository-ruleset-props.d.ts +7 -4
- package/dist/github/repository-ruleset-props.d.ts.map +1 -1
- package/dist/github/repository-ruleset-providers.d.ts +42 -0
- package/dist/github/repository-ruleset-providers.d.ts.map +1 -0
- package/dist/github/repository-ruleset.d.ts +26 -4
- package/dist/github/repository-ruleset.d.ts.map +1 -1
- package/dist/litellm/fake-litellm.d.ts +27 -16
- package/dist/litellm/fake-litellm.d.ts.map +1 -1
- package/dist/litellm/fake-stack.d.ts +9 -4
- package/dist/litellm/fake-stack.d.ts.map +1 -1
- package/dist/litellm/index.d.ts +7 -5
- package/dist/litellm/index.d.ts.map +1 -1
- package/dist/litellm/index.js +53 -123
- package/dist/litellm/index.js.map +8 -8
- package/dist/litellm/operations.d.ts +60 -0
- package/dist/litellm/operations.d.ts.map +1 -0
- package/dist/litellm/pass-through-endpoint.d.ts +10 -28
- package/dist/litellm/pass-through-endpoint.d.ts.map +1 -1
- package/dist/litellm/pass-through-errors.d.ts +37 -0
- package/dist/litellm/pass-through-errors.d.ts.map +1 -0
- package/dist/litellm/pass-through-form.d.ts +11 -4
- package/dist/litellm/pass-through-form.d.ts.map +1 -1
- package/dist/litellm/providers.d.ts +7 -5
- package/dist/litellm/providers.d.ts.map +1 -1
- package/dist/netbox/prefix.d.ts +4 -4
- package/dist/paperless/custom-field.d.ts +21 -21
- package/dist/paperless/custom-field.d.ts.map +1 -1
- package/dist/paperless/document-type.d.ts +34 -4
- package/dist/paperless/document-type.d.ts.map +1 -1
- package/dist/paperless/fake-paperless.d.ts +36 -19
- package/dist/paperless/fake-paperless.d.ts.map +1 -1
- package/dist/paperless/index.d.ts +7 -2
- package/dist/paperless/index.d.ts.map +1 -1
- package/dist/paperless/index.js +164 -228
- package/dist/paperless/index.js.map +10 -13
- package/dist/paperless/matching-locate.d.ts +42 -20
- package/dist/paperless/matching-locate.d.ts.map +1 -1
- package/dist/paperless/matching-types.d.ts +47 -18
- package/dist/paperless/matching-types.d.ts.map +1 -1
- package/dist/paperless/matching.d.ts +30 -27
- package/dist/paperless/matching.d.ts.map +1 -1
- package/dist/paperless/providers.d.ts +13 -7
- package/dist/paperless/providers.d.ts.map +1 -1
- package/dist/paperless/storage-path.d.ts +34 -4
- package/dist/paperless/storage-path.d.ts.map +1 -1
- package/dist/paperless/tag.d.ts +43 -8
- package/dist/paperless/tag.d.ts.map +1 -1
- package/dist/proxmox/acl.d.ts +30 -23
- package/dist/proxmox/acl.d.ts.map +1 -1
- package/dist/proxmox/distilled-pve.d.ts +89 -0
- package/dist/proxmox/distilled-pve.d.ts.map +1 -0
- package/dist/proxmox/index.js +663 -609
- package/dist/proxmox/index.js.map +12 -11
- package/dist/proxmox/members.d.ts +29 -10
- package/dist/proxmox/members.d.ts.map +1 -1
- package/docs/caddy-admin.md +1 -1
- package/docs/caddy.md +3 -2
- package/docs/credentials.md +16 -0
- package/docs/litellm.md +21 -13
- package/docs/paperless.md +12 -0
- package/docs/peers.md +5 -0
- package/docs/repo-policy.md +16 -1
- package/docs/repository-ruleset.md +53 -13
- package/docs/upstream-conformance.md +126 -10
- package/package.json +5 -2
- package/src/caddy/admin-address.ts +71 -0
- package/src/caddy/admin-calls.ts +48 -100
- package/src/caddy/admin.ts +37 -41
- package/src/caddy/adopt-scope.test.ts +1 -1
- package/src/caddy/caddy-http-client.ts +229 -0
- package/src/caddy/config-adopt-provider.test.ts +139 -0
- package/src/caddy/config-adopt.test.ts +60 -126
- package/src/caddy/config-errors.ts +47 -0
- package/src/caddy/config-lifecycle.test.ts +55 -42
- package/src/caddy/config-lifecycle.ts +72 -162
- package/src/caddy/config-reconcile.ts +138 -0
- package/src/caddy/config.ts +36 -42
- package/src/caddy/fake-caddy.ts +20 -2
- package/src/caddy/index.ts +3 -8
- package/src/caddy/local-admin.test.ts +35 -21
- package/src/caddy/local-admin.ts +60 -167
- package/src/caddy/providers.test.ts +14 -6
- package/src/caddy/providers.ts +20 -4
- package/src/github/declare-repo-baseline.ts +6 -6
- package/src/github/index.ts +8 -4
- package/src/github/repo-baseline-data.test.ts +13 -0
- package/src/github/repo-baseline-data.ts +11 -2
- package/src/github/repo-policy-stack.test.ts +59 -0
- package/src/github/repo-policy.ts +3 -2
- package/src/github/repository-ruleset-errors.ts +6 -4
- package/src/github/repository-ruleset-exact-adopt-k1.test.ts +109 -0
- package/src/github/repository-ruleset-exact-adopt.test.ts +14 -9
- package/src/github/repository-ruleset-form.ts +7 -3
- package/src/github/repository-ruleset-guards.ts +3 -2
- package/src/github/repository-ruleset-octokit.test.ts +141 -0
- package/src/github/repository-ruleset-octokit.ts +121 -30
- package/src/github/repository-ruleset-probe.ts +2 -1
- package/src/github/repository-ruleset-props.ts +7 -4
- package/src/github/repository-ruleset-providers.ts +46 -0
- package/src/github/repository-ruleset-wire.test.ts +38 -27
- package/src/github/repository-ruleset.ts +27 -3
- package/src/litellm/fake-litellm.ts +83 -78
- package/src/litellm/fake-stack.ts +13 -5
- package/src/litellm/index.ts +7 -21
- package/src/litellm/operations.ts +149 -0
- package/src/litellm/pass-through-endpoint-delete-swallow.test.ts +17 -20
- package/src/litellm/pass-through-endpoint-replace.test.ts +3 -4
- package/src/litellm/pass-through-endpoint.test.ts +8 -8
- package/src/litellm/pass-through-endpoint.ts +21 -31
- package/src/litellm/pass-through-errors.ts +37 -0
- package/src/litellm/pass-through-form.ts +20 -10
- package/src/litellm/providers.ts +8 -7
- package/src/paperless/custom-field.test.ts +49 -40
- package/src/paperless/custom-field.ts +57 -33
- package/src/paperless/document-type.test.ts +48 -0
- package/src/paperless/document-type.ts +51 -22
- package/src/paperless/fake-paperless.ts +63 -72
- package/src/paperless/index.ts +7 -13
- package/src/paperless/matching-locate.test.ts +102 -0
- package/src/paperless/matching-locate.ts +80 -86
- package/src/paperless/matching-types.ts +49 -16
- package/src/paperless/matching.ts +77 -90
- package/src/paperless/providers.ts +14 -10
- package/src/paperless/storage-path.test.ts +56 -0
- package/src/paperless/storage-path.ts +53 -24
- package/src/paperless/tag-identity.test.ts +127 -0
- package/src/paperless/tag.test.ts +100 -0
- package/src/paperless/tag.ts +62 -32
- package/src/proxmox/acl.test.ts +145 -0
- package/src/proxmox/acl.ts +128 -150
- package/src/proxmox/distilled-pve.test.ts +231 -0
- package/src/proxmox/distilled-pve.ts +147 -0
- package/src/proxmox/members-timeout.test.ts +161 -0
- package/src/proxmox/members.test.ts +9 -0
- package/src/proxmox/members.ts +36 -2
- package/dist/litellm/client.d.ts +0 -76
- package/dist/litellm/client.d.ts.map +0 -1
- package/dist/litellm/credentials.d.ts +0 -48
- package/dist/litellm/credentials.d.ts.map +0 -1
- package/dist/litellm/generated/pass-through.d.ts +0 -71
- package/dist/litellm/generated/pass-through.d.ts.map +0 -1
- package/dist/paperless/client.d.ts +0 -37
- package/dist/paperless/client.d.ts.map +0 -1
- package/dist/paperless/credentials.d.ts +0 -40
- package/dist/paperless/credentials.d.ts.map +0 -1
- package/dist/paperless/errors.d.ts +0 -49
- package/dist/paperless/errors.d.ts.map +0 -1
- package/src/litellm/client.ts +0 -195
- package/src/litellm/credentials.ts +0 -73
- package/src/litellm/generated/pass-through.ts +0 -73
- package/src/paperless/client.ts +0 -88
- package/src/paperless/credentials.ts +0 -69
- package/src/paperless/errors.ts +0 -62
- package/src/paperless/matching-identity.test.ts +0 -194
- package/src/paperless/matching.test.ts +0 -128
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `soleMatch`, `locateOne` and the PR 163 identity switch (`fetchLive`) — identity resolution, in
|
|
3
|
+
* isolation. Pure/Effect only (no HTTP, `R = never`): every fixture is `Effect.succeed`, mirroring
|
|
4
|
+
* `../netbox/resource.test.ts`'s own `locateOne` tests. `tag.test.ts`/`tag-identity.test.ts`
|
|
5
|
+
* exercise the real distilled wire path this feeds from, plus the full PR 163 regression suite
|
|
6
|
+
* this file does not repeat.
|
|
7
|
+
*
|
|
8
|
+
* ⛔ THE FAILURE `soleMatch` GUARDS IS SILENT AND SELF-RENEWING. If an ambiguous match took the
|
|
9
|
+
* first row, adopt would bind to it, the next plan would bind to the other, and every plan
|
|
10
|
+
* after that would report drift that is not there. Failing loudly is the only outcome an
|
|
11
|
+
* operator can act on. Ported verbatim from the pre-migration `client.test.ts`.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, test } from 'bun:test';
|
|
14
|
+
import * as Effect from 'effect/Effect';
|
|
15
|
+
import { LOCATE_PAGE, fetchLive, locateOne, soleMatch } from './matching-locate.ts';
|
|
16
|
+
|
|
17
|
+
describe('soleMatch', () => {
|
|
18
|
+
test('no candidate is absent, not an error — that is what plans a create', () => {
|
|
19
|
+
expect(soleMatch([], 'tags Invoices')).toBeUndefined();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('exactly one candidate is the object', () => {
|
|
23
|
+
expect(soleMatch([{ id: 7 }], 'tags Invoices')).toEqual({ id: 7 });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** ⛔ Two rows means the identity rule is wrong, and the message says which object it was. */
|
|
27
|
+
test('more than one candidate throws and names the object', () => {
|
|
28
|
+
expect(() => soleMatch([{ id: 7 }, { id: 8 }], 'tags Invoices')).toThrow(
|
|
29
|
+
/tags Invoices matched 2 Paperless objects/,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
interface Row {
|
|
35
|
+
readonly id: number;
|
|
36
|
+
readonly name: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const page = (results: readonly Row[]) => Effect.succeed({ count: results.length, results });
|
|
40
|
+
|
|
41
|
+
describe('locateOne', () => {
|
|
42
|
+
test('an empty page is absent', async () => {
|
|
43
|
+
expect(await Effect.runPromise(locateOne(page([]), 'tags Invoices'))).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('one row with no `identifies` narrowing is the object', async () => {
|
|
47
|
+
const row = await Effect.runPromise(
|
|
48
|
+
locateOne(page([{ id: 7, name: 'Invoices' }]), 'tags Invoices'),
|
|
49
|
+
);
|
|
50
|
+
expect(row).toEqual({ id: 7, name: 'Invoices' });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('`identifies` narrows several server-side matches to the declared one', async () => {
|
|
54
|
+
const rows = [
|
|
55
|
+
{ id: 7, name: 'Invoices' },
|
|
56
|
+
{ id: 8, name: 'invoices' },
|
|
57
|
+
];
|
|
58
|
+
const row = await Effect.runPromise(
|
|
59
|
+
locateOne(page(rows), 'tags Invoices', (r) => r.name === 'invoices'),
|
|
60
|
+
);
|
|
61
|
+
expect(row).toEqual({ id: 8, name: 'invoices' });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('more rows than the page width is a defect, not a truncation', async () => {
|
|
65
|
+
const rows = Array.from({ length: LOCATE_PAGE + 1 }, (_, i) => ({ id: i, name: 'x' }));
|
|
66
|
+
await expect(Effect.runPromise(locateOne(page(rows), 'tags Invoices'))).rejects.toThrow(
|
|
67
|
+
/matched 21 rows, more than the 20-row page/,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('fetchLive (the PR 163 identity switch)', () => {
|
|
73
|
+
const byName = (rows: readonly Row[]) => (props: { name: string }) =>
|
|
74
|
+
locateOne(page(rows), `tags ${props.name}`, (r) => r.name === props.name);
|
|
75
|
+
const byId = (rows: readonly Row[]) => (id: number) =>
|
|
76
|
+
Effect.succeed(rows.find((r) => r.id === id));
|
|
77
|
+
const describe_ = (props: { name: string }) => `tags ${props.name}`;
|
|
78
|
+
|
|
79
|
+
test('no output: locates by name', async () => {
|
|
80
|
+
const rows = [{ id: 7, name: 'Invoices' }];
|
|
81
|
+
const live = await Effect.runPromise(
|
|
82
|
+
fetchLive({ name: 'Invoices' }, undefined, byName(rows), byId(rows), describe_),
|
|
83
|
+
);
|
|
84
|
+
expect(live?.id).toBe(7);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('output present: locates by id, ignoring a since-changed name', async () => {
|
|
88
|
+
const rows = [{ id: 7, name: 'Renamed' }];
|
|
89
|
+
const live = await Effect.runPromise(
|
|
90
|
+
fetchLive({ name: 'Invoices' }, { id: 7 }, byName(rows), byId(rows), describe_),
|
|
91
|
+
);
|
|
92
|
+
expect(live?.id).toBe(7);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('output.id no longer live: refuses rather than falling through to a create', async () => {
|
|
96
|
+
await expect(
|
|
97
|
+
Effect.runPromise(
|
|
98
|
+
fetchLive({ name: 'Invoices' }, { id: 7 }, byName([]), byId([]), describe_),
|
|
99
|
+
),
|
|
100
|
+
).rejects.toThrow(/no longer exists live|deleted out of band/);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -4,101 +4,95 @@
|
|
|
4
4
|
* live Paperless row IS this declaration — by name while there is no state, by the stored
|
|
5
5
|
* `output.id` once there is one. See `matching.ts`'s header for the PR 163 red-team HIGH finding
|
|
6
6
|
* this identity switch fixes.
|
|
7
|
+
*
|
|
8
|
+
* ★ EVERY EXPORT HERE IS GENERIC OVER `R`, NOT TIED TO `PaperlessNgxOpContext` — mirroring
|
|
9
|
+
* `../netbox/resource.ts`'s standalone `locateOne`. `matching.ts` wires these against a real
|
|
10
|
+
* spec's `PaperlessNgxOpContext`-typed operations; `matching-locate.test.ts` wires them against
|
|
11
|
+
* plain `Effect.succeed` fixtures (`R = never`) — the same function, no fake HTTP needed to
|
|
12
|
+
* prove the pure rule.
|
|
13
|
+
*
|
|
14
|
+
* ⚠️ `NotFound` IS FOLDED TO "ABSENT" INSIDE EACH RESOURCE FILE'S OWN `spec.getById`, NOT HERE —
|
|
15
|
+
* the direct successor to the old, centralized `absentOn404`, and the same split
|
|
16
|
+
* `../netbox/resource.ts` and `../forgejo/resource.ts` use. `Effect.catchTag`'s tag-literal
|
|
17
|
+
* inference needs a concrete error union to resolve `"NotFound"` against; a spec's `E` is only
|
|
18
|
+
* concrete at each resource file's own call site (`GetTagError`, `GetDocumentTypeError`, …). A
|
|
19
|
+
* LIST call is never folded here either — an empty `results` page is a normal 200, not a 404
|
|
20
|
+
* (measured live, kit PR 194).
|
|
7
21
|
*/
|
|
8
22
|
import * as Effect from 'effect/Effect';
|
|
9
|
-
import { PaperlessNotFound } from './errors.ts';
|
|
10
|
-
import { type PaperlessList, type PaperlessRow, paperless } from './client.ts';
|
|
11
|
-
import type { MatchingError, MatchingProps, MatchingRequirements } from './matching-types.ts';
|
|
12
23
|
|
|
13
24
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
25
|
+
* ★ Wide enough to see every case-variant candidate `name__iexact` returns; a natural key past it
|
|
26
|
+
* is not one. Unchanged from the pre-migration `client.ts`'s own value.
|
|
16
27
|
*/
|
|
17
|
-
export const
|
|
18
|
-
io.pipe(
|
|
19
|
-
Effect.catchIf(
|
|
20
|
-
(cause): cause is PaperlessNotFound => cause instanceof PaperlessNotFound,
|
|
21
|
-
() => Effect.succeed(undefined as A),
|
|
22
|
-
),
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
/** ★ Wide enough to see every case-variant candidate `name__iexact` returns; a natural key past it is not one. */
|
|
26
|
-
const LOCATE_PAGE = 20;
|
|
27
|
-
|
|
28
|
-
/** ⚠️ Exported — `matching.ts`'s `patchBody`/`matches` compare the live `owner` too (PR 163). */
|
|
29
|
-
export const ownerOf = (row: PaperlessRow): number | undefined =>
|
|
30
|
-
typeof row['owner'] === 'number' ? row['owner'] : undefined;
|
|
28
|
+
export const LOCATE_PAGE = 20;
|
|
31
29
|
|
|
32
30
|
/**
|
|
33
|
-
* The
|
|
34
|
-
*
|
|
31
|
+
* The single row a filtered list returns, or `undefined`.
|
|
32
|
+
*
|
|
33
|
+
* ⛔ MORE THAN ONE MATCH IS A DEFECT, NOT A REASON TO TAKE THE FIRST. If an ambiguous match took
|
|
34
|
+
* the first row, adopt would bind to it, the next plan would bind to the other, and every plan
|
|
35
|
+
* after that would report drift that is not there — while "fixing" it PATCHed one object with
|
|
36
|
+
* the other's declaration. Failing loudly is the only outcome an operator can act on.
|
|
35
37
|
*/
|
|
36
|
-
export const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
})
|
|
40
|
-
/**
|
|
41
|
-
* ★ THE NO-STATE LOCATE. Used only when there is no `output` yet: a genuine first create, or
|
|
42
|
-
* the engine's own adoption probe (`provider.read`, always called with `output: undefined`).
|
|
43
|
-
*/
|
|
44
|
-
const fetchByName = (props: Props) =>
|
|
45
|
-
absentOn404(
|
|
46
|
-
paperless<PaperlessList<PaperlessRow>>(
|
|
47
|
-
'GET',
|
|
48
|
-
`${spec.collection}/?name__iexact=${encodeURIComponent(props.name)}&page_size=${String(LOCATE_PAGE)}`,
|
|
49
|
-
),
|
|
50
|
-
).pipe(
|
|
51
|
-
Effect.map((list) => {
|
|
52
|
-
if (list !== undefined && list.count > LOCATE_PAGE) {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`${spec.describe(props)}: name__iexact matched ${String(list.count)} rows, more than ` +
|
|
55
|
-
`the ${String(LOCATE_PAGE)}-row page this reads. Narrow the name rather than paging.`,
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
const rows = (list?.results ?? []).filter(
|
|
59
|
-
(row) => row['name'] === props.name && ownerOf(row) === (props.owner ?? undefined),
|
|
60
|
-
);
|
|
61
|
-
if (rows.length > 1) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`${spec.describe(props)}: matched ${String(rows.length)} Paperless objects with the same (name, owner).`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
return rows[0];
|
|
67
|
-
}),
|
|
38
|
+
export const soleMatch = <T>(rows: readonly T[], describe: string): T | undefined => {
|
|
39
|
+
if (rows.length > 1) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`${describe} matched ${String(rows.length)} Paperless objects with the same (name, owner).`,
|
|
68
42
|
);
|
|
43
|
+
}
|
|
44
|
+
return rows[0];
|
|
45
|
+
};
|
|
69
46
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
: fetchById(output.id).pipe(
|
|
87
|
-
Effect.flatMap((live) =>
|
|
88
|
-
live === undefined
|
|
89
|
-
? Effect.die(
|
|
90
|
-
new Error(
|
|
91
|
-
`${spec.describe(props)}: id ${String(output.id)} is recorded in state but no ` +
|
|
92
|
-
'longer exists live (deleted out of band). Refusing to create a replacement ' +
|
|
93
|
-
'— adopt the live object back under this declaration, or remove it from ' +
|
|
94
|
-
'state, before deploying again.',
|
|
95
|
-
),
|
|
96
|
-
)
|
|
97
|
-
: Effect.succeed(live),
|
|
98
|
-
),
|
|
47
|
+
/**
|
|
48
|
+
* List, then narrow to at most one candidate. Any Paperless resource whose locate is "filter
|
|
49
|
+
* server-side, then disambiguate in this process" reaches this once rather than reimplementing
|
|
50
|
+
* the page-width assertion and the ambiguity defect per resource.
|
|
51
|
+
*/
|
|
52
|
+
export const locateOne = <Live, E, R>(
|
|
53
|
+
list: Effect.Effect<{ readonly count: number; readonly results: readonly Live[] }, E, R>,
|
|
54
|
+
describe: string,
|
|
55
|
+
identifies?: (row: Live) => boolean,
|
|
56
|
+
): Effect.Effect<Live | undefined, E, R> =>
|
|
57
|
+
list.pipe(
|
|
58
|
+
Effect.map((page) => {
|
|
59
|
+
if (page.count > LOCATE_PAGE) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`${describe}: name__iexact matched ${String(page.count)} rows, more than the ` +
|
|
62
|
+
`${String(LOCATE_PAGE)}-row page this reads. Narrow the name rather than paging.`,
|
|
99
63
|
);
|
|
64
|
+
}
|
|
65
|
+
const rows = identifies === undefined ? page.results : page.results.filter(identifies);
|
|
66
|
+
return soleMatch(rows, describe);
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
100
69
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
70
|
+
/**
|
|
71
|
+
* ⛔ THE IDENTITY SWITCH (PR 163, red-team HIGH finding — see `matching.ts`'s header). `output`
|
|
72
|
+
* defined means this declaration has already written something: identity is `output.id`, full
|
|
73
|
+
* stop, and a changed `name`/`owner` in the caller's props is drift to PATCH, never a reason to
|
|
74
|
+
* relocate. `output` undefined means there is nothing to be wrong about yet — locate by name.
|
|
75
|
+
*/
|
|
76
|
+
export const fetchLive = <Props, Live, Attributes extends { readonly id: number }, E, R>(
|
|
77
|
+
props: Props,
|
|
78
|
+
output: Attributes | undefined,
|
|
79
|
+
fetchByName: (props: Props) => Effect.Effect<Live | undefined, E, R>,
|
|
80
|
+
fetchById: (id: number) => Effect.Effect<Live | undefined, E, R>,
|
|
81
|
+
describe: (props: Props) => string,
|
|
82
|
+
): Effect.Effect<Live | undefined, E, R> =>
|
|
83
|
+
output === undefined
|
|
84
|
+
? fetchByName(props)
|
|
85
|
+
: fetchById(output.id).pipe(
|
|
86
|
+
Effect.flatMap((live) =>
|
|
87
|
+
live === undefined
|
|
88
|
+
? Effect.die(
|
|
89
|
+
new Error(
|
|
90
|
+
`${describe(props)}: id ${String(output.id)} is recorded in state but no ` +
|
|
91
|
+
'longer exists live (deleted out of band). Refusing to create a replacement ' +
|
|
92
|
+
'— adopt the live object back under this declaration, or remove it from ' +
|
|
93
|
+
'state, before deploying again.',
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
: Effect.succeed(live),
|
|
97
|
+
),
|
|
98
|
+
);
|
|
@@ -4,21 +4,31 @@
|
|
|
4
4
|
* itself (the identity/locate/create/patch logic the PR 163 red-team HIGH finding is about) stays
|
|
5
5
|
* under the house's 250-line-per-file cap. See `matching.ts`'s header for the identity design
|
|
6
6
|
* (`output.id` once a resource has state) this `MatchingSpec` shape supports.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 MOVED ONTO `@distilled.cloud/paperless-ngx` (2026-09-24, decision 43) — `Live` is now
|
|
9
|
+
* whatever a distilled operation decodes (a `tags.Tag`, a `documentTypes.DocumentType`, …), no
|
|
10
|
+
* more untyped `PaperlessRow`. `E` is left to each resource file to declare (the union of the
|
|
11
|
+
* distilled operations it actually calls), so a call site's precise per-operation error union
|
|
12
|
+
* flows straight through instead of being widened by hand here — the same split
|
|
13
|
+
* `../netbox/resource.ts` and `../forgejo/resource.ts` use.
|
|
7
14
|
*/
|
|
8
|
-
import type {
|
|
9
|
-
import type
|
|
10
|
-
import type
|
|
11
|
-
import type { EndpointKey } from './constraints.ts';
|
|
15
|
+
import type { PaperlessNgxOpContext } from '@distilled.cloud/paperless-ngx/Protocol';
|
|
16
|
+
import type * as Effect from 'effect/Effect';
|
|
17
|
+
import type * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
18
|
+
import type { EndpointKey, PaperlessBody } from './constraints.ts';
|
|
12
19
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
/**
|
|
21
|
+
* What a CONSUMING STACK must provide. `matchingHandlers` (matching.ts) bakes in
|
|
22
|
+
* `CredentialsFromEnv` itself, the same way `netboxHandlers`/`forgejoHandlers` do, so only the
|
|
23
|
+
* transport is left — see matching.ts's own note on `withCredentials`.
|
|
24
|
+
*/
|
|
25
|
+
export type MatchingRequirements = HttpClient.HttpClient;
|
|
16
26
|
|
|
17
27
|
/** A wire field beyond `name`/`owner`: how to read it from props, and how to read it back. */
|
|
18
|
-
export interface FieldSpec<Props> {
|
|
28
|
+
export interface FieldSpec<Props, Live> {
|
|
19
29
|
readonly key: string;
|
|
20
30
|
readonly prop: (props: Props) => unknown;
|
|
21
|
-
readonly live: (row:
|
|
31
|
+
readonly live: (row: Live) => unknown;
|
|
22
32
|
}
|
|
23
33
|
|
|
24
34
|
export interface MatchingProps {
|
|
@@ -27,29 +37,40 @@ export interface MatchingProps {
|
|
|
27
37
|
readonly owner?: number;
|
|
28
38
|
}
|
|
29
39
|
|
|
40
|
+
/** The shape every `list*` distilled operation this family calls answers. */
|
|
41
|
+
export interface MatchingPage<Live> {
|
|
42
|
+
readonly count: number;
|
|
43
|
+
readonly results: readonly Live[];
|
|
44
|
+
}
|
|
45
|
+
|
|
30
46
|
export interface MatchingSpec<
|
|
31
47
|
Props extends MatchingProps,
|
|
48
|
+
Live extends { readonly id: number; readonly name: string },
|
|
32
49
|
Attributes extends { readonly id: number },
|
|
50
|
+
E,
|
|
33
51
|
> {
|
|
34
|
-
/** `tags`, `document_types`, `storage_paths`, `custom_fields`. */
|
|
35
|
-
readonly collection: string;
|
|
36
52
|
/**
|
|
37
53
|
* ⚠️ DEFAULT `true`. `Tag`, `DocumentType` and `StoragePath` extend Paperless-ngx's
|
|
38
54
|
* `OwnedObjectSerializer`; `CustomField` does not — its wire body has no `owner` key at all
|
|
39
55
|
* (measured against the v3.1.1 schema: `CustomFieldRequest` is `{name, data_type,
|
|
40
|
-
* extra_data}`). `false` stops `createBody` sending a field the vendor would silently ignore
|
|
56
|
+
* extra_data}`). `false` stops `createBody` sending a field the vendor would silently ignore,
|
|
57
|
+
* and `ownerOf` need not be declared.
|
|
41
58
|
*/
|
|
42
59
|
readonly owned?: boolean;
|
|
43
60
|
/** Compared, PATCHed when they differ, and sent on create. */
|
|
44
|
-
readonly fields: readonly FieldSpec<Props>[];
|
|
61
|
+
readonly fields: readonly FieldSpec<Props, Live>[];
|
|
45
62
|
/**
|
|
46
63
|
* Sent on CREATE only — never compared, never PATCHed. `CustomField.dataType` (custom-field.ts)
|
|
47
64
|
* is the one user: it must reach the create body, but `immutable` is what refuses a later
|
|
48
65
|
* change, and `matches`/`patchBody` must never see it as ordinary drift.
|
|
49
66
|
*/
|
|
50
|
-
readonly createOnly?: readonly FieldSpec<Props>[];
|
|
51
|
-
readonly attributes: (live:
|
|
67
|
+
readonly createOnly?: readonly FieldSpec<Props, Live>[];
|
|
68
|
+
readonly attributes: (live: Live, props: Props) => Attributes | undefined;
|
|
69
|
+
/** Reads the live row's owner. Required unless `owned === false` (CustomField). */
|
|
70
|
+
readonly ownerOf?: (live: Live) => number | undefined;
|
|
71
|
+
/** The generated constraint table to check each body against, by endpoint key. */
|
|
52
72
|
readonly endpoint: { readonly create: EndpointKey; readonly update?: EndpointKey };
|
|
73
|
+
/** For `soleMatch`'s message and for read-back failures. */
|
|
53
74
|
readonly describe: (props: Props) => string;
|
|
54
75
|
/**
|
|
55
76
|
* A field this object refuses to change once set — `CustomField.dataType` (custom-field.ts):
|
|
@@ -57,5 +78,17 @@ export interface MatchingSpec<
|
|
|
57
78
|
* replace is acceptable. Return the refusal message, or `undefined` when unchanged. Checked in
|
|
58
79
|
* `diff` (plan time) and again in `reconcile` (an adoption `diff` never saw).
|
|
59
80
|
*/
|
|
60
|
-
readonly immutable?: (live:
|
|
81
|
+
readonly immutable?: (live: Live, props: Props) => string | undefined;
|
|
82
|
+
|
|
83
|
+
/** `name__iexact` + `page_size` — wired per resource file (`listTags`, `listDocumentTypes`, …). */
|
|
84
|
+
readonly list: (props: Props) => Effect.Effect<MatchingPage<Live>, E, PaperlessNgxOpContext>;
|
|
85
|
+
/** Already folds the SDK's `NotFound` to `undefined` — each resource file's own `.pipe(Effect.catchTag('NotFound', …))`. */
|
|
86
|
+
readonly getById: (id: number) => Effect.Effect<Live | undefined, E, PaperlessNgxOpContext>;
|
|
87
|
+
readonly create: (body: PaperlessBody) => Effect.Effect<Live, E, PaperlessNgxOpContext>;
|
|
88
|
+
/** Absent would mean create-only-and-never-updated; every family here declares one. */
|
|
89
|
+
readonly update?: (
|
|
90
|
+
id: number,
|
|
91
|
+
body: PaperlessBody,
|
|
92
|
+
) => Effect.Effect<Live, E, PaperlessNgxOpContext>;
|
|
93
|
+
readonly destroy: (id: number) => Effect.Effect<unknown, E, PaperlessNgxOpContext>;
|
|
61
94
|
}
|