@homeflare/alchemy 0.27.4 → 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 +9 -5
- package/dist/github/index.d.ts.map +1 -1
- package/dist/github/index.js +161 -25
- package/dist/github/index.js.map +15 -13
- 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 +37 -3
- 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 +15 -2
- package/dist/github/repository-ruleset-guards.d.ts.map +1 -1
- package/dist/github/repository-ruleset-narrowing-guards.d.ts +39 -0
- package/dist/github/repository-ruleset-narrowing-guards.d.ts.map +1 -0
- 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 +91 -0
- package/dist/github/repository-ruleset-props.d.ts.map +1 -0
- 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-reconcile.d.ts.map +1 -1
- package/dist/github/repository-ruleset.d.ts +28 -34
- 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 +9 -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 +10 -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 +51 -4
- package/src/github/repository-ruleset-exact-adopt-k1.test.ts +109 -0
- package/src/github/repository-ruleset-exact-adopt.test.ts +184 -0
- package/src/github/repository-ruleset-form.ts +7 -3
- package/src/github/repository-ruleset-guards.ts +11 -4
- package/src/github/repository-ruleset-narrowing-guards.ts +141 -0
- package/src/github/repository-ruleset-narrowing.test.ts +210 -0
- 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 +95 -0
- package/src/github/repository-ruleset-providers.ts +46 -0
- package/src/github/repository-ruleset-reconcile.ts +35 -0
- package/src/github/repository-ruleset-wire.test.ts +38 -27
- package/src/github/repository-ruleset-write-refusals.test.ts +6 -26
- package/src/github/repository-ruleset.ts +53 -43
- 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
|
@@ -1,59 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A
|
|
3
|
-
*
|
|
2
|
+
* A minimal in-memory Paperless-ngx REST API for this family's tests — an in-memory `fetch`
|
|
3
|
+
* handed to Effect's real `FetchHttpClient` through its `Fetch` reference, mirroring
|
|
4
|
+
* `../netbox/fake-netbox.ts` and `../forgejo/fake-forgejo.ts`. Tests exercise distilled's REAL
|
|
5
|
+
* path assembly, JSON encode/decode and status→typed-error matching (`404` → `NotFound`, `403` →
|
|
6
|
+
* `Forbidden`, `400` → `BadRequest`, …) — nothing about that matching is re-implemented here, only
|
|
7
|
+
* the wire responses and in-memory rows a scenario needs.
|
|
4
8
|
*
|
|
5
|
-
* ⛔ TEST-ONLY. No provider imports this file.
|
|
6
|
-
* taxonomy collections and answers `name__iexact` the way Django does — case-insensitive,
|
|
7
|
-
*
|
|
9
|
+
* ⛔ TEST-ONLY, AND NO NETWORK. No provider imports this file. Stores rows in memory for the four
|
|
10
|
+
* taxonomy collections and answers `name__iexact` the way Django does — case-insensitive, never
|
|
11
|
+
* collapsing two differently-cased rows into one. Ported from the pre-migration `Bun.serve`
|
|
12
|
+
* fake: production now goes through `@distilled.cloud/paperless-ngx`'s `CredentialsFromEnv`,
|
|
13
|
+
* which resolves through Effect `Config` and snapshots `process.env` once (measured in the
|
|
14
|
+
* forgejo migration, kit PR 180) — a test can no longer point production code at a fake server
|
|
15
|
+
* by mutating env vars, so this hands an explicit `credentials()` layer instead.
|
|
8
16
|
*/
|
|
17
|
+
import { type Credentials, credentials } from '@distilled.cloud/paperless-ngx/Credentials';
|
|
18
|
+
import type { PaperlessNgxOpContext } from '@distilled.cloud/paperless-ngx/Protocol';
|
|
9
19
|
import * as Effect from 'effect/Effect';
|
|
20
|
+
import * as Layer from 'effect/Layer';
|
|
10
21
|
import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
|
|
11
|
-
import type { PaperlessRequirements } from './client.ts';
|
|
12
|
-
import { type PaperlessCredentialsError, environmentLayer } from './credentials.ts';
|
|
13
|
-
import type { PaperlessError } from './errors.ts';
|
|
14
22
|
|
|
15
|
-
export const
|
|
23
|
+
export const FAKE_BASE = 'https://paperless.example.com';
|
|
24
|
+
export const FAKE_TOKEN = 'placeholder-paperless-token';
|
|
16
25
|
|
|
17
26
|
export interface Seen {
|
|
18
27
|
readonly method: string;
|
|
19
28
|
readonly path: string;
|
|
20
|
-
readonly headers: Headers;
|
|
21
29
|
readonly body: string;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
export type Row = Record<string, unknown> & { id: number };
|
|
25
33
|
|
|
26
34
|
const COLLECTIONS = ['tags', 'document_types', 'storage_paths', 'custom_fields'] as const;
|
|
35
|
+
type Collection = (typeof COLLECTIONS)[number];
|
|
27
36
|
|
|
28
37
|
export interface FakePaperless {
|
|
29
|
-
readonly
|
|
38
|
+
readonly fetch: typeof globalThis.fetch;
|
|
30
39
|
readonly seen: Seen[];
|
|
31
|
-
readonly rows: (collection:
|
|
32
|
-
/** `undefined` to answer normally; a status to force it once
|
|
40
|
+
readonly rows: (collection: Collection) => readonly Row[];
|
|
41
|
+
/** `undefined` to answer normally; a status to force it once for the NEXT call only. */
|
|
33
42
|
forceStatus: number | undefined;
|
|
34
|
-
stop(): void;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
const nextId = (store: Row[]): number =>
|
|
45
|
+
const nextId = (store: readonly Row[]): number =>
|
|
38
46
|
store.length === 0 ? 1 : Math.max(...store.map((r) => r.id)) + 1;
|
|
39
47
|
|
|
48
|
+
const json = (status: number, body: unknown): Response =>
|
|
49
|
+
new Response(status === 204 ? null : JSON.stringify(body), {
|
|
50
|
+
headers: { 'content-type': 'application/json' },
|
|
51
|
+
status,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A stateful CRUD router across the four collections — this is what makes it possible to test
|
|
56
|
+
* `matching.ts`'s full create → read-back → PATCH → DELETE lifecycle, not just one canned
|
|
57
|
+
* response per test the way `fakeNetbox`/`fakeForgejo`'s simpler `route` callback does.
|
|
58
|
+
*/
|
|
40
59
|
export const fakePaperless = (): FakePaperless => {
|
|
41
60
|
const seen: Seen[] = [];
|
|
42
|
-
const store = new Map<
|
|
61
|
+
const store = new Map<Collection, Row[]>(COLLECTIONS.map((c) => [c, []]));
|
|
43
62
|
let forced: number | undefined;
|
|
44
63
|
|
|
45
|
-
const fetch = async (
|
|
64
|
+
const fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
65
|
+
const request =
|
|
66
|
+
input instanceof Request ? new Request(input, init) : new Request(String(input), init);
|
|
46
67
|
const url = new URL(request.url);
|
|
47
68
|
const body = await request.text();
|
|
48
|
-
seen.push({
|
|
49
|
-
|
|
50
|
-
headers: request.headers,
|
|
51
|
-
method: request.method,
|
|
52
|
-
path: `${url.pathname}${url.search}`,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const token = request.headers.get('Authorization');
|
|
56
|
-
if (token !== `Token ${FAKE_TOKEN}`) return json(401, { detail: 'Invalid token.' });
|
|
69
|
+
seen.push({ body, method: request.method, path: `${url.pathname}${url.search}` });
|
|
70
|
+
|
|
57
71
|
if (forced !== undefined) {
|
|
58
72
|
const status = forced;
|
|
59
73
|
forced = undefined;
|
|
@@ -62,7 +76,7 @@ export const fakePaperless = (): FakePaperless => {
|
|
|
62
76
|
|
|
63
77
|
const segments = url.pathname.split('/').filter((s) => s !== '');
|
|
64
78
|
// ['api', collection, (id)?]
|
|
65
|
-
const collection = segments[1] as
|
|
79
|
+
const collection = segments[1] as Collection | undefined;
|
|
66
80
|
if (collection === undefined || !COLLECTIONS.includes(collection))
|
|
67
81
|
return json(404, { detail: 'Not found.' });
|
|
68
82
|
const rows = store.get(collection) as Row[];
|
|
@@ -96,10 +110,10 @@ export const fakePaperless = (): FakePaperless => {
|
|
|
96
110
|
return new Response(null, { status: 204 });
|
|
97
111
|
}
|
|
98
112
|
return json(405, { detail: 'method not allowed' });
|
|
99
|
-
};
|
|
113
|
+
}) as typeof globalThis.fetch;
|
|
100
114
|
|
|
101
|
-
const server = Bun.serve({ fetch, hostname: '127.0.0.1', port: 0 });
|
|
102
115
|
return {
|
|
116
|
+
fetch,
|
|
103
117
|
get forceStatus() {
|
|
104
118
|
return forced;
|
|
105
119
|
},
|
|
@@ -108,49 +122,26 @@ export const fakePaperless = (): FakePaperless => {
|
|
|
108
122
|
},
|
|
109
123
|
rows: (collection) => store.get(collection) ?? [],
|
|
110
124
|
seen,
|
|
111
|
-
stop: () => void server.stop(true),
|
|
112
|
-
url: server.url.origin,
|
|
113
125
|
};
|
|
114
126
|
};
|
|
115
127
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
else process.env['PAPERLESS_TOKEN'] = savedToken;
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
/** Run an effect through the real `FetchHttpClient` and the real environment credentials layer. */
|
|
141
|
-
export const run = <A>(
|
|
142
|
-
effect: Effect.Effect<A, PaperlessError | PaperlessCredentialsError, PaperlessRequirements>,
|
|
143
|
-
) =>
|
|
144
|
-
Effect.runPromise(
|
|
145
|
-
effect.pipe(Effect.provide(FetchHttpClient.layer), Effect.provide(environmentLayer)),
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
export const runFailure = <A>(
|
|
149
|
-
effect: Effect.Effect<A, PaperlessError | PaperlessCredentialsError, PaperlessRequirements>,
|
|
150
|
-
) =>
|
|
151
|
-
Effect.runPromise(
|
|
152
|
-
Effect.flip(effect).pipe(
|
|
153
|
-
Effect.provide(FetchHttpClient.layer),
|
|
154
|
-
Effect.provide(environmentLayer),
|
|
155
|
-
),
|
|
156
|
-
);
|
|
128
|
+
/**
|
|
129
|
+
* distilled Credentials + the real FetchHttpClient over the fake — `PaperlessNgxOpContext`, what
|
|
130
|
+
* `matchingOperations(spec)` (matching.ts, unwrapped — not `matchingHandlers`, which bakes in
|
|
131
|
+
* `CredentialsFromEnv` and so cannot be pointed at a fake server, per this file's own header)
|
|
132
|
+
* needs to run against a fake.
|
|
133
|
+
*/
|
|
134
|
+
export const fakePaperlessLayer = (
|
|
135
|
+
fetchFn: typeof globalThis.fetch,
|
|
136
|
+
creds: Layer.Layer<Credentials> = credentials({ baseUrl: FAKE_BASE, token: FAKE_TOKEN }),
|
|
137
|
+
) => Layer.mergeAll(creds, FetchHttpClient.layer, Layer.succeed(FetchHttpClient.Fetch, fetchFn));
|
|
138
|
+
|
|
139
|
+
export const run = <A, E>(
|
|
140
|
+
effect: Effect.Effect<A, E, PaperlessNgxOpContext>,
|
|
141
|
+
fetchFn: typeof globalThis.fetch,
|
|
142
|
+
) => Effect.runPromise(effect.pipe(Effect.provide(fakePaperlessLayer(fetchFn))));
|
|
143
|
+
|
|
144
|
+
export const runFailure = <A, E>(
|
|
145
|
+
effect: Effect.Effect<A, E, PaperlessNgxOpContext>,
|
|
146
|
+
fetchFn: typeof globalThis.fetch,
|
|
147
|
+
) => Effect.runPromise(Effect.flip(effect).pipe(Effect.provide(fakePaperlessLayer(fetchFn))));
|
package/src/paperless/index.ts
CHANGED
|
@@ -5,20 +5,14 @@
|
|
|
5
5
|
* header. `matching.ts`'s internals and the raw generated modules are not re-exported; a
|
|
6
6
|
* consumer wanting the same guard a Resource uses gets it through `constraintsFor`/
|
|
7
7
|
* `bodyViolations`, not by importing `generated/` directly.
|
|
8
|
+
*
|
|
9
|
+
* 🔴 `PaperlessCredentials`/`PaperlessError` AND FRIENDS ARE GONE (2026-09-24, the
|
|
10
|
+
* `@distilled.cloud/paperless-ngx` migration — matching.ts's header), the same way
|
|
11
|
+
* `../netbox/index.ts` dropped `NetboxError`/`apiBase` in its own migration. A caller that
|
|
12
|
+
* needs the typed errors now imports them from `@distilled.cloud/paperless-ngx` directly (e.g.
|
|
13
|
+
* `@distilled.cloud/paperless-ngx/tags`'s `NotFound`); nothing in this estate imported the old
|
|
14
|
+
* names (measured 2026-09-24).
|
|
8
15
|
*/
|
|
9
|
-
export {
|
|
10
|
-
PaperlessCredentials,
|
|
11
|
-
type PaperlessCredentialValue,
|
|
12
|
-
PaperlessCredentialsError,
|
|
13
|
-
environmentLayer as paperlessEnvironmentLayer,
|
|
14
|
-
} from './credentials.ts';
|
|
15
|
-
export {
|
|
16
|
-
PaperlessNotFound,
|
|
17
|
-
PaperlessUnauthorized,
|
|
18
|
-
PaperlessValidation,
|
|
19
|
-
PaperlessUnavailable,
|
|
20
|
-
type PaperlessError,
|
|
21
|
-
} from './errors.ts';
|
|
22
16
|
export {
|
|
23
17
|
PAPERLESS_CONSTRAINTS,
|
|
24
18
|
bodyViolations,
|
|
@@ -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
|
}
|