@homeflare/alchemy 0.28.0 → 0.29.1
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 +83 -24
- package/dist/github/index.js.map +14 -12
- package/dist/github/repo-apply-order-fake-providers.d.ts +48 -0
- package/dist/github/repo-apply-order-fake-providers.d.ts.map +1 -0
- package/dist/github/repo-apply-order-fake-stack.d.ts +13 -0
- package/dist/github/repo-apply-order-fake-stack.d.ts.map +1 -0
- package/dist/github/repo-auto-merge-gate.d.ts +54 -0
- package/dist/github/repo-auto-merge-gate.d.ts.map +1 -0
- 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-apply-order.test.ts +86 -0
- package/src/github/declare-repo-baseline.ts +40 -7
- package/src/github/index.ts +8 -4
- package/src/github/repo-apply-order-fake-providers.ts +119 -0
- package/src/github/repo-apply-order-fake-stack.ts +74 -0
- package/src/github/repo-auto-merge-gate.ts +57 -0
- package/src/github/repo-baseline-data.test.ts +13 -0
- package/src/github/repo-baseline-data.ts +11 -2
- package/src/github/repo-policy-apply-order.test.ts +72 -0
- package/src/github/repo-policy-stack.test.ts +59 -0
- package/src/github/repo-policy.ts +31 -3
- 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
|
@@ -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
|
}
|
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
* One shape for every Paperless taxonomy object (Tag, DocumentType, StoragePath, CustomField):
|
|
3
3
|
* locate by `name__iexact` only while there is no prior state, identify the exact (name, owner),
|
|
4
4
|
* create-or-update, never delete by default. Modelled on `../netbox/resource.ts`'s
|
|
5
|
-
* `netboxOperations
|
|
5
|
+
* `netboxOperations` and `../forgejo/resource.ts`'s `forgejoOperations`.
|
|
6
|
+
*
|
|
7
|
+
* 🔴 WHY THIS REPLACED `client.ts`'s HAND-ROLLED `HttpClient` CALLS (2026-09-24, decision 43).
|
|
8
|
+
* Measured against Tim's distilled research page 2026-09-23: upstream Alchemy only accepts
|
|
9
|
+
* providers that call `@distilled.cloud/<vendor>` operations and `catchTag` their typed errors
|
|
10
|
+
* — not a raw `fetch`/`HttpClient` client over a hand-maintained path table.
|
|
11
|
+
* `@distilled.cloud/paperless-ngx` (aliased onto `@homeflare/distilled-paperless-ngx@0.3.0` —
|
|
12
|
+
* docs/distilled-interim.md) covers every operation this family calls; its 404/400 shapes were
|
|
13
|
+
* already live-measured (kit PR 194) before this migration touched a resource file.
|
|
6
14
|
*
|
|
7
15
|
* ⛔ THE CREATE BODY ALWAYS CARRIES `owner`, NULL WHEN UNDECLARED. `OwnedObjectSerializer.create`
|
|
8
16
|
* sets the owner to the TOKEN USER when the field is absent from the body (Paperless-ngx
|
|
@@ -10,60 +18,38 @@
|
|
|
10
18
|
* omits an undeclared foreign key would silently create an object owned by whichever credential
|
|
11
19
|
* ran the deploy, invisible to every other user and a second row beside the estate's `owner:
|
|
12
20
|
* null` one on the next apply. Sending `owner: null` explicitly is what keeps a create
|
|
13
|
-
* idempotent under this vendor's default.
|
|
21
|
+
* idempotent under this vendor's default. Unchanged by the transport swap.
|
|
14
22
|
*
|
|
15
23
|
* ★ READ ANSWERS `Unowned(attrs)` ON EVERY MATCH (H1, alchemy-provider-standard). Paperless has no
|
|
16
24
|
* ownership marker this package could check — `owner` here is the estate's OWN prop, not proof
|
|
17
25
|
* of who created the row — so, per the house's 2026-09-21 decision, identical is not ours: a
|
|
18
|
-
* live match never adopts silently.
|
|
19
|
-
* marker-less API (upstream's `Snippet.ts#read`).
|
|
26
|
+
* live match never adopts silently.
|
|
20
27
|
*
|
|
21
28
|
* ⛔ IDENTITY IS THE STORED `id`, NOT `name`/`owner`, ONCE A RESOURCE HAS STATE (PR 163, red-team
|
|
22
|
-
* HIGH finding
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* right there holding the object's real id. Editing `name` (or `owner`, for the three owned
|
|
26
|
-
* families) on an already-deployed resource is the single most ordinary edit anyone would make
|
|
27
|
-
* — and it made `fetchLive(news)` miss the row it previously wrote, fall into the create branch,
|
|
28
|
-
* POST a SECOND object under the new name/owner, and leave the original permanently orphaned
|
|
29
|
-
* (`list()` always answers `[]` by design, so `nuke` can never find it either). `diff` reported
|
|
30
|
-
* a plain `{action:'update'}` — nothing told the operator this was about to duplicate and
|
|
31
|
-
* abandon an object.
|
|
32
|
-
*
|
|
33
|
-
* The fix: once `output` is defined, `fetchLive` locates by `output.id` (a direct
|
|
34
|
-
* `GET {collection}/{id}/`), never by name — a changed `name`/`owner` becomes an ordinary PATCH
|
|
35
|
-
* of that same id (Paperless allows PATCHing both on all four types; documents keep their
|
|
36
|
-
* tag/type/path because the id never changes). `patchBody`/`matches` now compare `name` and
|
|
37
|
-
* (when `spec.owned !== false`) `owner` against the live row too, so the rename/re-own actually
|
|
38
|
-
* reaches the PATCH body — locating the right row was necessary but not sufficient. The
|
|
39
|
-
* name/owner-based `fetchByName` locate survives ONLY for the no-state path: a genuine first
|
|
40
|
-
* create, or the engine's own adoption probe (`provider.read`, called with `output: undefined`
|
|
41
|
-
* per `Plan.ts`'s adopt block) — `read` is unchanged and still locates by name for exactly that
|
|
42
|
-
* reason. If `output.id` no longer exists live (deleted out of band — by hand, or by another
|
|
43
|
-
* tool), `fetchLive` refuses loudly (`Effect.die`) rather than silently falling through to a
|
|
44
|
-
* create: a plan that would otherwise read as "ordinary update" instead names the id and tells
|
|
45
|
-
* the operator to adopt or clean up state, the same posture `immutable` already uses below for
|
|
46
|
-
* `CustomField.dataType`.
|
|
29
|
+
* HIGH finding — see `matching-locate.ts`'s `fetchLive` for the fix and the full history). This
|
|
30
|
+
* migration keeps that fix byte-for-byte: only the wire call under `fetchByName`/`fetchById`
|
|
31
|
+
* changed, not the switch itself.
|
|
47
32
|
*/
|
|
48
33
|
import { Unowned } from 'alchemy/AdoptPolicy';
|
|
49
34
|
import { isResolved } from 'alchemy/Diff';
|
|
50
35
|
import type { Input } from 'alchemy/Input';
|
|
36
|
+
import { CredentialsFromEnv } from '@distilled.cloud/paperless-ngx/Credentials';
|
|
37
|
+
import type { PaperlessNgxOpContext } from '@distilled.cloud/paperless-ngx/Protocol';
|
|
51
38
|
import * as Effect from 'effect/Effect';
|
|
52
|
-
import type { PaperlessRow } from './client.ts';
|
|
53
|
-
import { paperless } from './client.ts';
|
|
54
39
|
import { guardBody } from './constraint-guard.ts';
|
|
55
40
|
import type { PaperlessBody } from './constraints.ts';
|
|
56
|
-
import {
|
|
41
|
+
import { fetchLive as identitySwitch, locateOne } from './matching-locate.ts';
|
|
57
42
|
import type { MatchingProps, MatchingSpec } from './matching-types.ts';
|
|
58
43
|
|
|
59
44
|
/** Re-exported so `tag.ts`/`document-type.ts`/`storage-path.ts`/`custom-field.ts` keep importing the contract from `./matching.ts` — only the shape itself lives in `matching-types.ts`, split out to hold the file-size cap. */
|
|
60
45
|
export type {
|
|
61
46
|
FieldSpec,
|
|
62
|
-
|
|
47
|
+
MatchingPage,
|
|
63
48
|
MatchingProps,
|
|
64
49
|
MatchingRequirements,
|
|
65
50
|
MatchingSpec,
|
|
66
51
|
} from './matching-types.ts';
|
|
52
|
+
export { LOCATE_PAGE, soleMatch } from './matching-locate.ts';
|
|
67
53
|
|
|
68
54
|
/** Object equality good enough for this family's scalars, nullable ints and `extra_data` JSON. */
|
|
69
55
|
const sameValue = (a: unknown, b: unknown): boolean =>
|
|
@@ -71,15 +57,35 @@ const sameValue = (a: unknown, b: unknown): boolean =>
|
|
|
71
57
|
|
|
72
58
|
export const matchingOperations = <
|
|
73
59
|
Props extends MatchingProps,
|
|
60
|
+
Live extends { readonly id: number; readonly name: string },
|
|
74
61
|
Attributes extends { readonly id: number },
|
|
62
|
+
E,
|
|
75
63
|
>(
|
|
76
|
-
spec: MatchingSpec<Props, Attributes>,
|
|
64
|
+
spec: MatchingSpec<Props, Live, Attributes, E>,
|
|
77
65
|
) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
66
|
+
const ownerOf = (live: Live): number | undefined =>
|
|
67
|
+
spec.owned === false || spec.ownerOf === undefined ? undefined : spec.ownerOf(live);
|
|
68
|
+
|
|
69
|
+
const ownerMatches = (live: Live, props: Props): boolean =>
|
|
70
|
+
spec.owned === false || spec.ownerOf === undefined
|
|
71
|
+
? true
|
|
72
|
+
: spec.ownerOf(live) === (props.owner ?? undefined);
|
|
73
|
+
|
|
74
|
+
/** ★ THE NO-STATE LOCATE — see matching-locate.ts's `locateOne` and this file's own header. */
|
|
75
|
+
const fetchByName = (props: Props) =>
|
|
76
|
+
locateOne(
|
|
77
|
+
spec.list(props),
|
|
78
|
+
spec.describe(props),
|
|
79
|
+
(row) => row.name === props.name && ownerMatches(row, props),
|
|
80
|
+
);
|
|
81
81
|
|
|
82
|
-
/**
|
|
82
|
+
/** Already folds a 404 — each resource file's own `.pipe(Effect.catchTag('NotFound', …))`. */
|
|
83
|
+
const fetchById = spec.getById;
|
|
84
|
+
|
|
85
|
+
const fetchLive = (props: Props, output: Attributes | undefined) =>
|
|
86
|
+
identitySwitch(props, output, fetchByName, fetchById, spec.describe);
|
|
87
|
+
|
|
88
|
+
/** ⚠️ ALWAYS NAME-BASED — this is `read`'s own contract (S7): find the object from props alone, with no `output` to consult. */
|
|
83
89
|
const read = (props: Props) =>
|
|
84
90
|
fetchByName(props).pipe(
|
|
85
91
|
Effect.map((live) => (live === undefined ? undefined : spec.attributes(live, props))),
|
|
@@ -102,17 +108,10 @@ export const matchingOperations = <
|
|
|
102
108
|
return out;
|
|
103
109
|
};
|
|
104
110
|
|
|
105
|
-
/**
|
|
106
|
-
|
|
107
|
-
* still a write the audit log carries for nothing.
|
|
108
|
-
* ⛔ `name` AND (WHEN OWNED) `owner` ARE COMPARED TOO, NOT JUST `spec.fields`. Locating the
|
|
109
|
-
* right row by `id` fixed WHERE the write lands; without this, a rename/re-own would still
|
|
110
|
-
* locate correctly and then send an empty-for-those-keys PATCH, so the live object's name or
|
|
111
|
-
* owner would silently never change (PR 163).
|
|
112
|
-
*/
|
|
113
|
-
const patchBody = (props: Props, live: PaperlessRow): PaperlessBody => {
|
|
111
|
+
/** ⚠️ Only fields that DIFFER from the live row. `name`/`owner` are compared too (PR 163). */
|
|
112
|
+
const patchBody = (props: Props, live: Live): PaperlessBody => {
|
|
114
113
|
const out: PaperlessBody = {};
|
|
115
|
-
if (props.name !== live
|
|
114
|
+
if (props.name !== live.name) out['name'] = props.name;
|
|
116
115
|
if (spec.owned !== false && ownerOf(live) !== (props.owner ?? undefined)) {
|
|
117
116
|
out['owner'] = props.owner ?? null;
|
|
118
117
|
}
|
|
@@ -123,8 +122,8 @@ export const matchingOperations = <
|
|
|
123
122
|
return out;
|
|
124
123
|
};
|
|
125
124
|
|
|
126
|
-
const matches = (props: Props, live:
|
|
127
|
-
if (props.name !== live
|
|
125
|
+
const matches = (props: Props, live: Live): boolean => {
|
|
126
|
+
if (props.name !== live.name) return false;
|
|
128
127
|
if (spec.owned !== false && ownerOf(live) !== (props.owner ?? undefined)) return false;
|
|
129
128
|
return spec.fields.every((field) => {
|
|
130
129
|
const value = field.prop(props);
|
|
@@ -143,7 +142,7 @@ export const matchingOperations = <
|
|
|
143
142
|
const refusal = spec.immutable?.(live, news);
|
|
144
143
|
if (refusal !== undefined) return yield* Effect.die(new Error(refusal));
|
|
145
144
|
if (matches(news, live)) return { action: 'noop' } as const;
|
|
146
|
-
return spec.
|
|
145
|
+
return spec.update === undefined
|
|
147
146
|
? ({ action: 'replace' } as const)
|
|
148
147
|
: ({ action: 'update' } as const);
|
|
149
148
|
}),
|
|
@@ -155,25 +154,19 @@ export const matchingOperations = <
|
|
|
155
154
|
if (before === undefined) {
|
|
156
155
|
const body = createBody(news);
|
|
157
156
|
yield* guardBody(spec.endpoint.create, body, true);
|
|
158
|
-
yield*
|
|
157
|
+
yield* spec.create(body);
|
|
159
158
|
} else {
|
|
160
|
-
id =
|
|
161
|
-
// ⚠️ RE-CHECKED HERE, NOT ONLY IN `diff` — an adoption's first reconcile can run with
|
|
162
|
-
// no prior `diff` at all, and `immutable` guards a write that would destroy data.
|
|
159
|
+
id = before.id;
|
|
163
160
|
const refusal = spec.immutable?.(before, news);
|
|
164
161
|
if (refusal !== undefined) return yield* Effect.die(new Error(refusal));
|
|
165
|
-
if (spec.
|
|
162
|
+
if (spec.update !== undefined) {
|
|
166
163
|
const body = patchBody(news, before);
|
|
167
164
|
if (Object.keys(body).length > 0) {
|
|
168
165
|
yield* guardBody(spec.endpoint.update, body, false);
|
|
169
|
-
yield*
|
|
166
|
+
yield* spec.update(id, body);
|
|
170
167
|
}
|
|
171
168
|
}
|
|
172
169
|
}
|
|
173
|
-
// ★ READ BACK BY id WHEN WE HAVE ONE — a just-PATCHed row is read back by the id we
|
|
174
|
-
// wrote to, not by (possibly just-changed) name, so a rename's own read-back cannot
|
|
175
|
-
// fail the way the write it follows no longer can either. A fresh create has no id yet
|
|
176
|
-
// to read back by, so that branch still reads back by the name it just created.
|
|
177
170
|
const after = yield* id === undefined ? read(news) : readById(news, id);
|
|
178
171
|
if (after === undefined) {
|
|
179
172
|
return yield* Effect.die(
|
|
@@ -185,55 +178,49 @@ export const matchingOperations = <
|
|
|
185
178
|
return after;
|
|
186
179
|
}),
|
|
187
180
|
|
|
188
|
-
/**
|
|
189
|
-
* ⛔ `output.id` FIRST, SAME SWITCH AS `fetchLive` (PR 163 re-review). The engine always
|
|
190
|
-
* passes `output` here too (`Apply.ts`'s delete call sites all carry `output: attr`) —
|
|
191
|
-
* `olds` is only the PROPS as last persisted, which drifts from the live object exactly
|
|
192
|
-
* the way `news` did pre-fix: rename it out of band (by hand, or by another tool) after
|
|
193
|
-
* the last successful reconcile, and a name-only locate silently finds nothing, `destroy`
|
|
194
|
-
* returns as if there were nothing to delete, and the engine drops the state row anyway —
|
|
195
|
-
* reporting "deleted" while the live object survives, permanently orphaned (the same
|
|
196
|
-
* consequence `matching.ts`'s header describes for the original bug, on the delete path
|
|
197
|
-
* the original fix didn't touch). A 404 on the `output.id` itself is NOT an error here,
|
|
198
|
-
* unlike `fetchLive`'s die — it means the object is already gone, which is exactly the
|
|
199
|
-
* idempotent success `delete` is supposed to report (S11, alchemy-provider-standard).
|
|
200
|
-
*/
|
|
181
|
+
/** ⛔ `output.id` FIRST, SAME SWITCH AS `fetchLive` (PR 163 re-review). A 404 on `output.id` is idempotent success, not an error, unlike `fetchLive`'s die. */
|
|
201
182
|
destroy: (olds: Props, output: Attributes | undefined) =>
|
|
202
183
|
Effect.gen(function* () {
|
|
203
184
|
const live = output === undefined ? yield* fetchByName(olds) : yield* fetchById(output.id);
|
|
204
185
|
if (live === undefined) return;
|
|
205
|
-
yield*
|
|
186
|
+
yield* spec.destroy(live.id);
|
|
206
187
|
}),
|
|
207
188
|
};
|
|
208
189
|
};
|
|
209
190
|
|
|
210
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* The five provider handlers for a spec'd Paperless taxonomy object, wired once.
|
|
193
|
+
*
|
|
194
|
+
* ★ CREDENTIALS ARE PROVIDED HERE, ONCE, NOT BY EVERY CALLER — the same shape
|
|
195
|
+
* `netboxHandlers`/`forgejoHandlers` use. `CredentialsFromEnv` resolves `PAPERLESS_URL` /
|
|
196
|
+
* `PAPERLESS_TOKEN` on the calling fiber per request, so this closure keeps that laziness —
|
|
197
|
+
* nothing is captured at module load. ⛔ THE TOKEN IS NEVER A PROP.
|
|
198
|
+
*/
|
|
211
199
|
export const matchingHandlers = <
|
|
212
200
|
Props extends MatchingProps,
|
|
201
|
+
Live extends { readonly id: number; readonly name: string },
|
|
213
202
|
Attributes extends { readonly id: number },
|
|
203
|
+
E,
|
|
214
204
|
>(
|
|
215
|
-
spec: MatchingSpec<Props, Attributes>,
|
|
205
|
+
spec: MatchingSpec<Props, Live, Attributes, E>,
|
|
216
206
|
) => {
|
|
217
207
|
const ops = matchingOperations(spec);
|
|
208
|
+
const withCredentials = <A>(effect: Effect.Effect<A, E, PaperlessNgxOpContext>) =>
|
|
209
|
+
Effect.provide(effect, CredentialsFromEnv);
|
|
218
210
|
return {
|
|
219
211
|
/** ⚠️ `[]` — Paperless index endpoints return the whole estate; adoption stays explicit. */
|
|
220
212
|
list: () => Effect.succeed([]),
|
|
221
213
|
read: ({ olds }: { olds: Props }) =>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
214
|
+
withCredentials(
|
|
215
|
+
ops
|
|
216
|
+
.read(olds)
|
|
217
|
+
.pipe(Effect.map((attrs) => (attrs === undefined ? undefined : Unowned(attrs)))),
|
|
218
|
+
),
|
|
225
219
|
diff: ({ news, output }: { news: Input<Props>; output: Attributes | undefined }) =>
|
|
226
|
-
ops.diff(news, output),
|
|
227
|
-
// ⛔ `output` IS WHAT MAKES THE IDENTITY FIX WORK (PR 163) — the engine always passes it
|
|
228
|
-
// (`Provider.ts#reconcile`), so it is optional here only so a test can still call
|
|
229
|
-
// `reconcile({ news })` for a from-nothing create without spelling out `output: undefined`.
|
|
230
|
-
// ⚠️ `| undefined` on the optional type itself, not just `?` — `exactOptionalPropertyTypes`
|
|
231
|
-
// otherwise refuses the engine's own `output: Attributes | undefined` at the call site.
|
|
220
|
+
withCredentials(ops.diff(news, output)),
|
|
232
221
|
reconcile: ({ news, output }: { news: Props; output?: Attributes | undefined }) =>
|
|
233
|
-
ops.reconcile(news, output),
|
|
234
|
-
// ⚠️ `output` PASSED THROUGH, NOT DROPPED — see `destroy`'s own header above (PR 163
|
|
235
|
-
// re-review). Same `| undefined` reasoning as `reconcile` just above.
|
|
222
|
+
withCredentials(ops.reconcile(news, output)),
|
|
236
223
|
delete: ({ olds, output }: { olds: Props; output?: Attributes | undefined }) =>
|
|
237
|
-
ops.destroy(olds, output),
|
|
224
|
+
withCredentials(ops.destroy(olds, output)),
|
|
238
225
|
};
|
|
239
226
|
};
|
|
@@ -1,27 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* All four Paperless taxonomy providers
|
|
2
|
+
* All four Paperless taxonomy providers.
|
|
3
3
|
*
|
|
4
4
|
* Layer.mergeAll(paperlessProviders(), …the stack's other providers)
|
|
5
5
|
*
|
|
6
6
|
* ⚠️ `HttpClient.HttpClient` IS NOT BUNDLED HERE, and that follows `../netbox/index.ts` and
|
|
7
|
-
* `../forgejo/client.ts`'s own precedent
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* `../forgejo/client.ts`'s own precedent: the transport is the consuming STACK's choice
|
|
8
|
+
* (`FetchHttpClient.layer` in production, a fake `HttpClient` in a test), not a house default a
|
|
9
|
+
* provider file should pin.
|
|
10
|
+
*
|
|
11
|
+
* 🔴 NO `credentials` PARAMETER ANY MORE (2026-09-24, the `@distilled.cloud/paperless-ngx`
|
|
12
|
+
* migration — see `matching.ts`'s header). Each of the four `xxxProvider()`s now bakes in
|
|
13
|
+
* `CredentialsFromEnv` itself, the same way `netboxHandlers`/`forgejoHandlers` do — there is no
|
|
14
|
+
* longer a layer for this file to merge in or accept an override for. Nothing in this estate
|
|
15
|
+
* called `paperlessProviders(customLayer)` (measured 2026-09-24: no tray repo declares a
|
|
16
|
+
* Paperless resource at all — grep across `homeflare-landscape`'s tray repos' `origin/main`),
|
|
17
|
+
* so this is an uneventful simplification, not a behaviour change for anything live.
|
|
11
18
|
*/
|
|
12
19
|
import * as Layer from 'effect/Layer';
|
|
13
20
|
import { CustomFieldProvider } from './custom-field.ts';
|
|
14
21
|
import { DocumentTypeProvider } from './document-type.ts';
|
|
15
|
-
import { type PaperlessCredentials, environmentLayer } from './credentials.ts';
|
|
16
22
|
import { StoragePathProvider } from './storage-path.ts';
|
|
17
23
|
import { TagProvider } from './tag.ts';
|
|
18
24
|
|
|
19
|
-
export const paperlessProviders = (
|
|
20
|
-
credentials: Layer.Layer<PaperlessCredentials> = environmentLayer,
|
|
21
|
-
) =>
|
|
25
|
+
export const paperlessProviders = () =>
|
|
22
26
|
Layer.mergeAll(
|
|
23
27
|
TagProvider(),
|
|
24
28
|
DocumentTypeProvider(),
|
|
25
29
|
StoragePathProvider(),
|
|
26
30
|
CustomFieldProvider(),
|
|
27
|
-
)
|
|
31
|
+
);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `StoragePath`'s exported production `spec` against a fake Paperless-ngx — proves this file's
|
|
3
|
+
* own `@distilled.cloud/paperless-ngx/storage_paths` wiring (right operations, right field names,
|
|
4
|
+
* including the vendor-required `path` template) round-trips for real. See document-type.test.ts
|
|
5
|
+
* and tag.ts's own header for why the shared engine and PR 163 are not re-proven here.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, test } from 'bun:test';
|
|
8
|
+
import { fakePaperless, run } from './fake-paperless.ts';
|
|
9
|
+
import { matchingOperations } from './matching.ts';
|
|
10
|
+
import { spec } from './storage-path.ts';
|
|
11
|
+
|
|
12
|
+
const ops = matchingOperations(spec);
|
|
13
|
+
|
|
14
|
+
test('create sends the required path field alongside owner: null', async () => {
|
|
15
|
+
const fake = fakePaperless();
|
|
16
|
+
await run(
|
|
17
|
+
ops.reconcile({ name: 'By correspondent', path: '{correspondent}/{title}' }, undefined),
|
|
18
|
+
fake.fetch,
|
|
19
|
+
);
|
|
20
|
+
const post = fake.seen.find((s) => s.method === 'POST');
|
|
21
|
+
expect(JSON.parse(post?.body ?? '{}')).toMatchObject({
|
|
22
|
+
name: 'By correspondent',
|
|
23
|
+
owner: null,
|
|
24
|
+
path: '{correspondent}/{title}',
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('a full create → read → patch → delete round trip', () => {
|
|
29
|
+
test('the real distilled wire path carries the path template through a rename', async () => {
|
|
30
|
+
const fake = fakePaperless();
|
|
31
|
+
const created = await run(
|
|
32
|
+
ops.reconcile({ name: 'By correspondent', path: '{correspondent}/{title}' }, undefined),
|
|
33
|
+
fake.fetch,
|
|
34
|
+
);
|
|
35
|
+
expect(created).toMatchObject({ name: 'By correspondent', path: '{correspondent}/{title}' });
|
|
36
|
+
|
|
37
|
+
const after = await run(
|
|
38
|
+
ops.reconcile({ name: 'By correspondent', path: '{correspondent}/{created}' }, created),
|
|
39
|
+
fake.fetch,
|
|
40
|
+
);
|
|
41
|
+
const patch = fake.seen.find((s) => s.method === 'PATCH');
|
|
42
|
+
expect(JSON.parse(patch?.body ?? '{}')).toEqual({ path: '{correspondent}/{created}' });
|
|
43
|
+
expect(after?.path).toBe('{correspondent}/{created}');
|
|
44
|
+
|
|
45
|
+
await run(
|
|
46
|
+
ops.destroy({ name: 'By correspondent', path: '{correspondent}/{created}' }, after),
|
|
47
|
+
fake.fetch,
|
|
48
|
+
);
|
|
49
|
+
expect(fake.rows('storage_paths').length).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('a 404 on read folds to absent', async () => {
|
|
54
|
+
const fake = fakePaperless();
|
|
55
|
+
expect(await run(ops.read({ name: 'Nothing here', path: 'x' }), fake.fetch)).toBeUndefined();
|
|
56
|
+
});
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `Paperless.StoragePath` — a Paperless-ngx storage path template, create-or-update and never
|
|
3
|
-
* deleted. Same shape as `document-type.ts` plus the vendor-required `path` template string
|
|
3
|
+
* deleted. Same shape as `document-type.ts` plus the vendor-required `path` template string; see
|
|
4
|
+
* `tag.ts`'s header for the 2026-09-24 transport migration this file follows exactly.
|
|
4
5
|
*/
|
|
5
6
|
import { Resource } from 'alchemy';
|
|
6
7
|
import * as Provider from 'alchemy/Provider';
|
|
8
|
+
import * as storagePaths from '@distilled.cloud/paperless-ngx/storage_paths';
|
|
7
9
|
import * as Effect from 'effect/Effect';
|
|
8
10
|
import type { PaperlessStoragePathRequest } from './generated/types/storage-paths.ts';
|
|
9
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
LOCATE_PAGE,
|
|
13
|
+
type MatchingRequirements,
|
|
14
|
+
type MatchingSpec,
|
|
15
|
+
matchingHandlers,
|
|
16
|
+
} from './matching.ts';
|
|
10
17
|
|
|
11
|
-
export interface StoragePathProps
|
|
18
|
+
export interface StoragePathProps {
|
|
12
19
|
/** ⚠️ maxLength 128 (generated constraint table). The locate key. */
|
|
13
20
|
name: string;
|
|
21
|
+
owner?: number;
|
|
14
22
|
/** ⚠️ minLength 1 (generated table). Paperless's storage-path template syntax, e.g. `{correspondent}/{title}`. */
|
|
15
23
|
path: string;
|
|
16
24
|
/** ⚠️ maxLength 256. */
|
|
@@ -41,38 +49,59 @@ export const StoragePath = Resource<StoragePath>('Paperless.StoragePath', {
|
|
|
41
49
|
defaultRemovalPolicy: 'retain',
|
|
42
50
|
});
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
52
|
+
/** ★ Exported for direct testing — see tag.ts's own note on this seam. */
|
|
53
|
+
export const spec: MatchingSpec<
|
|
54
|
+
StoragePathProps,
|
|
55
|
+
storagePaths.StoragePath,
|
|
56
|
+
StoragePathAttributes,
|
|
57
|
+
| storagePaths.CreateStoragePathError
|
|
58
|
+
| storagePaths.GetStoragePathError
|
|
59
|
+
| storagePaths.ListStoragePathsError
|
|
60
|
+
| storagePaths.UpdateStoragePathsPartialError
|
|
61
|
+
| storagePaths.StoragePathsDestroyError
|
|
62
|
+
> = {
|
|
63
|
+
attributes: (live, props) => ({
|
|
64
|
+
id: live.id,
|
|
65
|
+
isInsensitive: live.is_insensitive === true,
|
|
66
|
+
match: live.match ?? '',
|
|
67
|
+
matchingAlgorithm: live.matching_algorithm ?? 0,
|
|
68
|
+
name: live.name,
|
|
69
|
+
owner: live.owner ?? undefined,
|
|
70
|
+
path: live.path,
|
|
71
|
+
}),
|
|
72
|
+
create: (body) =>
|
|
73
|
+
storagePaths.createStoragePath(body as unknown as storagePaths.CreateStoragePathRequest),
|
|
60
74
|
describe: (props) => `storage_paths ${props.name}`,
|
|
75
|
+
destroy: (id) => storagePaths.storagePathsDestroy({ id }),
|
|
61
76
|
endpoint: {
|
|
62
77
|
create: 'paperless:POST /api/storage_paths/',
|
|
63
78
|
update: 'paperless:PATCH /api/storage_paths/{id}/',
|
|
64
79
|
},
|
|
65
80
|
fields: [
|
|
66
|
-
{ key: 'path', live: (r) => r
|
|
67
|
-
{ key: 'match', live: (r) => r
|
|
81
|
+
{ key: 'path', live: (r) => r.path, prop: (p) => p.path },
|
|
82
|
+
{ key: 'match', live: (r) => r.match, prop: (p) => p.match },
|
|
68
83
|
{
|
|
69
84
|
key: 'matching_algorithm',
|
|
70
|
-
live: (r) => r
|
|
85
|
+
live: (r) => r.matching_algorithm,
|
|
71
86
|
prop: (p) => p.matchingAlgorithm,
|
|
72
87
|
},
|
|
73
|
-
{ key: 'is_insensitive', live: (r) => r
|
|
88
|
+
{ key: 'is_insensitive', live: (r) => r.is_insensitive, prop: (p) => p.isInsensitive },
|
|
74
89
|
],
|
|
75
|
-
|
|
90
|
+
getById: (id) =>
|
|
91
|
+
storagePaths
|
|
92
|
+
.getStoragePath({ id })
|
|
93
|
+
.pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),
|
|
94
|
+
list: (props) =>
|
|
95
|
+
storagePaths.listStoragePaths({ name__iexact: props.name, page_size: LOCATE_PAGE }),
|
|
96
|
+
ownerOf: (live) => live.owner ?? undefined,
|
|
97
|
+
update: (id, body) =>
|
|
98
|
+
storagePaths.updateStoragePathsPartial({
|
|
99
|
+
...(body as unknown as Omit<storagePaths.UpdateStoragePathsPartialRequest, 'id'>),
|
|
100
|
+
id,
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export const handlers = matchingHandlers(spec);
|
|
76
105
|
|
|
77
106
|
export const StoragePathProvider = () =>
|
|
78
107
|
Provider.effect(StoragePath, Effect.succeed(StoragePath.Provider.of(handlers)));
|