@homeflare/alchemy 0.23.0 → 0.25.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.
Files changed (177) hide show
  1. package/README.md +25 -1
  2. package/dist/litellm/client.d.ts +76 -0
  3. package/dist/litellm/client.d.ts.map +1 -0
  4. package/dist/litellm/credentials.d.ts +48 -0
  5. package/dist/litellm/credentials.d.ts.map +1 -0
  6. package/dist/litellm/fake-litellm.d.ts +36 -0
  7. package/dist/litellm/fake-litellm.d.ts.map +1 -0
  8. package/dist/litellm/fake-stack.d.ts +30 -0
  9. package/dist/litellm/fake-stack.d.ts.map +1 -0
  10. package/dist/litellm/generated/pass-through.d.ts +71 -0
  11. package/dist/litellm/generated/pass-through.d.ts.map +1 -0
  12. package/dist/litellm/index.d.ts +13 -0
  13. package/dist/litellm/index.d.ts.map +1 -0
  14. package/dist/litellm/index.js +270 -0
  15. package/dist/litellm/index.js.map +14 -0
  16. package/dist/litellm/pass-through-endpoint.d.ts +94 -0
  17. package/dist/litellm/pass-through-endpoint.d.ts.map +1 -0
  18. package/dist/litellm/pass-through-form.d.ts +40 -0
  19. package/dist/litellm/pass-through-form.d.ts.map +1 -0
  20. package/dist/litellm/providers.d.ts +16 -0
  21. package/dist/litellm/providers.d.ts.map +1 -0
  22. package/dist/paperless/client.d.ts +37 -0
  23. package/dist/paperless/client.d.ts.map +1 -0
  24. package/dist/paperless/constraint-guard.d.ts +19 -0
  25. package/dist/paperless/constraint-guard.d.ts.map +1 -0
  26. package/dist/paperless/constraints.d.ts +50 -0
  27. package/dist/paperless/constraints.d.ts.map +1 -0
  28. package/dist/paperless/credentials.d.ts +40 -0
  29. package/dist/paperless/credentials.d.ts.map +1 -0
  30. package/dist/paperless/custom-field.d.ts +75 -0
  31. package/dist/paperless/custom-field.d.ts.map +1 -0
  32. package/dist/paperless/document-type.d.ts +30 -0
  33. package/dist/paperless/document-type.d.ts.map +1 -0
  34. package/dist/paperless/errors.d.ts +49 -0
  35. package/dist/paperless/errors.d.ts.map +1 -0
  36. package/dist/paperless/fake-paperless.d.ts +39 -0
  37. package/dist/paperless/fake-paperless.d.ts.map +1 -0
  38. package/dist/paperless/generated/constraints/index.d.ts +13 -0
  39. package/dist/paperless/generated/constraints/index.d.ts.map +1 -0
  40. package/dist/paperless/generated/constraints/paperless-custom_fields.d.ts +21 -0
  41. package/dist/paperless/generated/constraints/paperless-custom_fields.d.ts.map +1 -0
  42. package/dist/paperless/generated/constraints/paperless-document_types.d.ts +21 -0
  43. package/dist/paperless/generated/constraints/paperless-document_types.d.ts.map +1 -0
  44. package/dist/paperless/generated/constraints/paperless-storage_paths.d.ts +21 -0
  45. package/dist/paperless/generated/constraints/paperless-storage_paths.d.ts.map +1 -0
  46. package/dist/paperless/generated/constraints/paperless-tags.d.ts +21 -0
  47. package/dist/paperless/generated/constraints/paperless-tags.d.ts.map +1 -0
  48. package/dist/paperless/generated/types/custom-fields.d.ts +23 -0
  49. package/dist/paperless/generated/types/custom-fields.d.ts.map +1 -0
  50. package/dist/paperless/generated/types/document-types.d.ts +39 -0
  51. package/dist/paperless/generated/types/document-types.d.ts.map +1 -0
  52. package/dist/paperless/generated/types/index.d.ts +10 -0
  53. package/dist/paperless/generated/types/index.d.ts.map +1 -0
  54. package/dist/paperless/generated/types/storage-paths.d.ts +31 -0
  55. package/dist/paperless/generated/types/storage-paths.d.ts.map +1 -0
  56. package/dist/paperless/generated/types/tags.d.ts +37 -0
  57. package/dist/paperless/generated/types/tags.d.ts.map +1 -0
  58. package/dist/paperless/index.d.ts +19 -0
  59. package/dist/paperless/index.d.ts.map +1 -0
  60. package/dist/paperless/index.js +524 -0
  61. package/dist/paperless/index.js.map +26 -0
  62. package/dist/paperless/matching-locate.d.ts +33 -0
  63. package/dist/paperless/matching-locate.d.ts.map +1 -0
  64. package/dist/paperless/matching-types.d.ts +60 -0
  65. package/dist/paperless/matching-types.d.ts.map +1 -0
  66. package/dist/paperless/matching.d.ts +61 -0
  67. package/dist/paperless/matching.d.ts.map +1 -0
  68. package/dist/paperless/providers.d.ts +15 -0
  69. package/dist/paperless/providers.d.ts.map +1 -0
  70. package/dist/paperless/storage-path.d.ts +32 -0
  71. package/dist/paperless/storage-path.d.ts.map +1 -0
  72. package/dist/paperless/tag.d.ts +47 -0
  73. package/dist/paperless/tag.d.ts.map +1 -0
  74. package/dist/postgres/connection.d.ts +50 -0
  75. package/dist/postgres/connection.d.ts.map +1 -0
  76. package/dist/postgres/database-attrs.d.ts +79 -0
  77. package/dist/postgres/database-attrs.d.ts.map +1 -0
  78. package/dist/postgres/database-sql.d.ts +73 -0
  79. package/dist/postgres/database-sql.d.ts.map +1 -0
  80. package/dist/postgres/database.d.ts +63 -0
  81. package/dist/postgres/database.d.ts.map +1 -0
  82. package/dist/postgres/errors.d.ts +86 -0
  83. package/dist/postgres/errors.d.ts.map +1 -0
  84. package/dist/postgres/fake-sql.d.ts +22 -0
  85. package/dist/postgres/fake-sql.d.ts.map +1 -0
  86. package/dist/postgres/index.d.ts +16 -0
  87. package/dist/postgres/index.d.ts.map +1 -0
  88. package/dist/postgres/index.js +238 -0
  89. package/dist/postgres/index.js.map +15 -0
  90. package/dist/postgres/providers.d.ts +14 -0
  91. package/dist/postgres/providers.d.ts.map +1 -0
  92. package/dist/proxmox/index.js.map +2 -2
  93. package/dist/proxmox/zfs-pool.d.ts +10 -2
  94. package/dist/proxmox/zfs-pool.d.ts.map +1 -1
  95. package/dist/release/archive.d.ts +6 -2
  96. package/dist/release/archive.d.ts.map +1 -1
  97. package/dist/release/binary-form.d.ts +14 -9
  98. package/dist/release/binary-form.d.ts.map +1 -1
  99. package/dist/release/binary-pins.d.ts +17 -0
  100. package/dist/release/binary-pins.d.ts.map +1 -0
  101. package/dist/release/catalog.d.ts.map +1 -1
  102. package/dist/release/index.js +90 -40
  103. package/dist/release/index.js.map +8 -7
  104. package/dist/release/tar.d.ts +9 -2
  105. package/dist/release/tar.d.ts.map +1 -1
  106. package/docs/litellm.md +110 -0
  107. package/docs/paperless.md +115 -0
  108. package/docs/peers.md +8 -1
  109. package/docs/postgres.md +125 -0
  110. package/docs/release-binary-catalogs.md +40 -4
  111. package/docs/release-binary-refusals.md +22 -8
  112. package/docs/release-binary-upstream.md +33 -15
  113. package/package.json +14 -1
  114. package/src/litellm/client.ts +195 -0
  115. package/src/litellm/credentials.ts +73 -0
  116. package/src/litellm/fake-litellm.ts +114 -0
  117. package/src/litellm/fake-stack.ts +100 -0
  118. package/src/litellm/generated/pass-through.ts +73 -0
  119. package/src/litellm/index.ts +42 -0
  120. package/src/litellm/pass-through-endpoint-delete-swallow.test.ts +66 -0
  121. package/src/litellm/pass-through-endpoint-replace.test.ts +84 -0
  122. package/src/litellm/pass-through-endpoint.test.ts +208 -0
  123. package/src/litellm/pass-through-endpoint.ts +258 -0
  124. package/src/litellm/pass-through-form.test.ts +128 -0
  125. package/src/litellm/pass-through-form.ts +136 -0
  126. package/src/litellm/providers.ts +19 -0
  127. package/src/paperless/client.ts +88 -0
  128. package/src/paperless/constraint-guard.ts +50 -0
  129. package/src/paperless/constraints.test.ts +74 -0
  130. package/src/paperless/constraints.ts +111 -0
  131. package/src/paperless/credentials.ts +69 -0
  132. package/src/paperless/custom-field.test.ts +74 -0
  133. package/src/paperless/custom-field.ts +95 -0
  134. package/src/paperless/document-type.ts +74 -0
  135. package/src/paperless/errors.ts +62 -0
  136. package/src/paperless/fake-paperless.ts +156 -0
  137. package/src/paperless/generated/constraints/index.ts +23 -0
  138. package/src/paperless/generated/constraints/paperless-custom_fields.ts +29 -0
  139. package/src/paperless/generated/constraints/paperless-document_types.ts +32 -0
  140. package/src/paperless/generated/constraints/paperless-storage_paths.ts +34 -0
  141. package/src/paperless/generated/constraints/paperless-tags.ts +34 -0
  142. package/src/paperless/generated/types/custom-fields.ts +23 -0
  143. package/src/paperless/generated/types/document-types.ts +30 -0
  144. package/src/paperless/generated/types/index.ts +9 -0
  145. package/src/paperless/generated/types/storage-paths.ts +31 -0
  146. package/src/paperless/generated/types/tags.ts +37 -0
  147. package/src/paperless/index.ts +56 -0
  148. package/src/paperless/matching-identity.test.ts +194 -0
  149. package/src/paperless/matching-locate.ts +104 -0
  150. package/src/paperless/matching-types.ts +61 -0
  151. package/src/paperless/matching.test.ts +128 -0
  152. package/src/paperless/matching.ts +239 -0
  153. package/src/paperless/providers.ts +27 -0
  154. package/src/paperless/storage-path.ts +78 -0
  155. package/src/paperless/tag.ts +91 -0
  156. package/src/postgres/connection.ts +62 -0
  157. package/src/postgres/database-attrs.ts +87 -0
  158. package/src/postgres/database-bigint-serialization.test.ts +140 -0
  159. package/src/postgres/database-lifecycle.test.ts +113 -0
  160. package/src/postgres/database-provenance.test.ts +118 -0
  161. package/src/postgres/database-refusals.test.ts +135 -0
  162. package/src/postgres/database-sql.ts +174 -0
  163. package/src/postgres/database.ts +193 -0
  164. package/src/postgres/errors.ts +131 -0
  165. package/src/postgres/fake-sql.ts +123 -0
  166. package/src/postgres/fixtures/pg-REL_18_6-create-database-synopsis.txt +26 -0
  167. package/src/postgres/fixtures/pg-REL_18_6-pg_database.h.txt +67 -0
  168. package/src/postgres/index.ts +30 -0
  169. package/src/postgres/providers.ts +16 -0
  170. package/src/proxmox/zfs-pool.ts +10 -2
  171. package/src/release/archive.ts +10 -4
  172. package/src/release/binary-form.ts +24 -76
  173. package/src/release/binary-pins.ts +95 -0
  174. package/src/release/binary-root.test.ts +122 -0
  175. package/src/release/catalog.ts +10 -2
  176. package/src/release/tar-root.test.ts +114 -0
  177. package/src/release/tar.ts +78 -12
@@ -0,0 +1,95 @@
1
+ /**
2
+ * `Paperless.CustomField` — a Paperless-ngx custom field, create-or-update and never deleted.
3
+ *
4
+ * ⛔ `dataType` IS CREATE-ONLY, ENFORCED AT PLAN TIME, NEVER A PATCH AND NEVER A REPLACE. A
5
+ * replace would delete this id and create a new one — and Paperless deletes the field's VALUE
6
+ * on every document when its custom field is deleted. `matching.ts`'s `immutable` hook carries
7
+ * this refusal generically; `dataType` is simply never listed in `fields`, so nothing in
8
+ * `matching.ts` ever builds a PATCH body that includes it.
9
+ *
10
+ * ⛔ NO `owner` FIELD. Unlike `Tag`/`DocumentType`/`StoragePath`, `CustomField` does not extend
11
+ * Paperless-ngx's `OwnedObjectSerializer` — its wire body is exactly `{name, data_type,
12
+ * extra_data}` (measured against the v3.1.1 schema). `owned: false` stops `matching.ts`
13
+ * sending a field the vendor's serializer does not declare.
14
+ *
15
+ * ⚠️ `CustomField.select_options` (the vendor's own rule for a `select` field's option list) is
16
+ * not in the served schema at all, so it is recorded here and NOT enforced (groundTruth).
17
+ */
18
+ import { Resource } from 'alchemy';
19
+ import * as Provider from 'alchemy/Provider';
20
+ import * as Effect from 'effect/Effect';
21
+ import type { PaperlessCustomFieldRequest } from './generated/types/custom-fields.ts';
22
+ import type { PaperlessRow } from './client.ts';
23
+ import { type MatchingRequirements, matchingHandlers } from './matching.ts';
24
+
25
+ export interface CustomFieldProps {
26
+ /** ⚠️ maxLength 128 (generated constraint table). The locate key — CustomField's name is globally unique (models.py:1169), with no owner to further scope it. */
27
+ name: string;
28
+ /**
29
+ * ⛔ CREATE-ONLY — see this file's header. `select`'s own option list
30
+ * (`select_options`) is not in the vendor schema and is unenforced here.
31
+ */
32
+ dataType: PaperlessCustomFieldRequest['data_type'];
33
+ /** Arbitrary JSON the vendor attaches no schema to (e.g. a `select` field's option list). */
34
+ extraData?: unknown;
35
+ }
36
+
37
+ export interface CustomFieldAttributes {
38
+ id: number;
39
+ name: string;
40
+ dataType: string;
41
+ extraData: unknown;
42
+ }
43
+
44
+ export interface CustomField extends Resource<
45
+ 'Paperless.CustomField',
46
+ CustomFieldProps,
47
+ CustomFieldAttributes,
48
+ never,
49
+ MatchingRequirements
50
+ > {}
51
+
52
+ export const CustomField = Resource<CustomField>('Paperless.CustomField', {
53
+ defaultRemovalPolicy: 'retain',
54
+ });
55
+
56
+ /**
57
+ * ⛔ EXTRACTED SO IT CAN BE TESTED WITHOUT A SERVER (custom-field.test.ts) — the same reason
58
+ * `../netbox/prefix-form.ts` pulls its body/matches functions out of `prefix.ts`.
59
+ */
60
+ export const dataTypeImmutable = (
61
+ live: PaperlessRow,
62
+ props: CustomFieldProps,
63
+ ): string | undefined =>
64
+ typeof live['data_type'] === 'string' && live['data_type'] !== props.dataType
65
+ ? `Paperless.CustomField "${props.name}": data_type is create-only (live: ${live['data_type']}, ` +
66
+ `declared: ${props.dataType}). Changing it would delete this field's value on every ` +
67
+ 'document. Create a new custom field instead, and retire this one by hand.'
68
+ : undefined;
69
+
70
+ /** ⚠️ Exported (not through index.ts) so custom-field.test.ts can drive the real lifecycle. */
71
+ export const customFieldHandlers = matchingHandlers<CustomFieldProps, CustomFieldAttributes>({
72
+ attributes: (live, props) => {
73
+ const id = live['id'];
74
+ if (typeof id !== 'number') return undefined;
75
+ return {
76
+ dataType: typeof live['data_type'] === 'string' ? live['data_type'] : props.dataType,
77
+ extraData: live['extra_data'],
78
+ id,
79
+ name: typeof live['name'] === 'string' ? live['name'] : props.name,
80
+ };
81
+ },
82
+ collection: 'custom_fields',
83
+ describe: (props) => `custom_fields ${props.name}`,
84
+ endpoint: {
85
+ create: 'paperless:POST /api/custom_fields/',
86
+ update: 'paperless:PATCH /api/custom_fields/{id}/',
87
+ },
88
+ createOnly: [{ key: 'data_type', live: (r) => r['data_type'], prop: (p) => p.dataType }],
89
+ fields: [{ key: 'extra_data', live: (r) => r['extra_data'], prop: (p) => p.extraData }],
90
+ immutable: dataTypeImmutable,
91
+ owned: false,
92
+ });
93
+
94
+ export const CustomFieldProvider = () =>
95
+ Provider.effect(CustomField, Effect.succeed(CustomField.Provider.of(customFieldHandlers)));
@@ -0,0 +1,74 @@
1
+ /**
2
+ * `Paperless.DocumentType` — a Paperless-ngx document type, create-or-update and never deleted.
3
+ * Same shape as `tag.ts` minus `color`/`isInboxTag`/`parent` — see that file's header for why
4
+ * `matchingAlgorithm`'s type comes off the generated request type rather than being retyped.
5
+ */
6
+ import { Resource } from 'alchemy';
7
+ import * as Provider from 'alchemy/Provider';
8
+ import * as Effect from 'effect/Effect';
9
+ import type { PaperlessDocumentTypeRequest } from './generated/types/document-types.ts';
10
+ import { type MatchingProps, type MatchingRequirements, matchingHandlers } from './matching.ts';
11
+
12
+ export interface DocumentTypeProps extends MatchingProps {
13
+ /** ⚠️ maxLength 128 (generated constraint table). The locate key. */
14
+ name: string;
15
+ /** ⚠️ maxLength 256. */
16
+ match?: string;
17
+ matchingAlgorithm?: PaperlessDocumentTypeRequest['matching_algorithm'];
18
+ isInsensitive?: boolean;
19
+ }
20
+
21
+ export interface DocumentTypeAttributes {
22
+ id: number;
23
+ name: string;
24
+ match: string;
25
+ matchingAlgorithm: number;
26
+ isInsensitive: boolean;
27
+ owner: number | undefined;
28
+ }
29
+
30
+ export interface DocumentType extends Resource<
31
+ 'Paperless.DocumentType',
32
+ DocumentTypeProps,
33
+ DocumentTypeAttributes,
34
+ never,
35
+ MatchingRequirements
36
+ > {}
37
+
38
+ export const DocumentType = Resource<DocumentType>('Paperless.DocumentType', {
39
+ defaultRemovalPolicy: 'retain',
40
+ });
41
+
42
+ const handlers = matchingHandlers<DocumentTypeProps, DocumentTypeAttributes>({
43
+ attributes: (live, props) => {
44
+ const id = live['id'];
45
+ if (typeof id !== 'number') return undefined;
46
+ return {
47
+ id,
48
+ isInsensitive: live['is_insensitive'] === true,
49
+ match: typeof live['match'] === 'string' ? live['match'] : '',
50
+ matchingAlgorithm:
51
+ typeof live['matching_algorithm'] === 'number' ? live['matching_algorithm'] : 0,
52
+ name: typeof live['name'] === 'string' ? live['name'] : props.name,
53
+ owner: typeof live['owner'] === 'number' ? live['owner'] : undefined,
54
+ };
55
+ },
56
+ collection: 'document_types',
57
+ describe: (props) => `document_types ${props.name}`,
58
+ endpoint: {
59
+ create: 'paperless:POST /api/document_types/',
60
+ update: 'paperless:PATCH /api/document_types/{id}/',
61
+ },
62
+ fields: [
63
+ { key: 'match', live: (r) => r['match'], prop: (p) => p.match },
64
+ {
65
+ key: 'matching_algorithm',
66
+ live: (r) => r['matching_algorithm'],
67
+ prop: (p) => p.matchingAlgorithm,
68
+ },
69
+ { key: 'is_insensitive', live: (r) => r['is_insensitive'], prop: (p) => p.isInsensitive },
70
+ ],
71
+ });
72
+
73
+ export const DocumentTypeProvider = () =>
74
+ Provider.effect(DocumentType, Effect.succeed(DocumentType.Provider.of(handlers)));
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Paperless-ngx's HTTP status codes, mapped to tagged errors exactly once — here, and never
3
+ * again by a status check inside a resource (S21, alchemy-provider-standard).
4
+ *
5
+ * ⛔ A SINGLE `PaperlessError` CLASS WITH A `.status` FIELD WOULD WORK, AND IS WHAT `netbox/
6
+ * client.ts` DOES. This family does it the S21 way instead: one tag per distinct MEANING, so a
7
+ * caller writes `Effect.catchTag('PaperlessNotFound', …)` rather than `if (e.status === 404)`.
8
+ * `matching.ts`'s "a 403 is never read as absent" rule is exactly what a shared tag buys —
9
+ * `catchTag('PaperlessNotFound', …)` cannot accidentally also catch `PaperlessUnauthorized`.
10
+ */
11
+ import * as Data from 'effect/Data';
12
+
13
+ /** No object at that path. The only tag `matching.ts` folds to "absent". */
14
+ export class PaperlessNotFound extends Data.TaggedError('PaperlessNotFound')<{
15
+ readonly method: string;
16
+ readonly path: string;
17
+ }> {}
18
+
19
+ /** 401 (no/bad token) or 403 (token valid, forbidden) — NEVER read as "the object is absent". */
20
+ export class PaperlessUnauthorized extends Data.TaggedError('PaperlessUnauthorized')<{
21
+ readonly status: 401 | 403;
22
+ readonly method: string;
23
+ readonly path: string;
24
+ }> {}
25
+
26
+ /** 400 — the vendor rejected the body. Carries it, so the message quotes Paperless's own words. */
27
+ export class PaperlessValidation extends Data.TaggedError('PaperlessValidation')<{
28
+ readonly method: string;
29
+ readonly path: string;
30
+ readonly body: string;
31
+ }> {}
32
+
33
+ /** 5xx, or a status this family's endpoints do not otherwise document. */
34
+ export class PaperlessUnavailable extends Data.TaggedError('PaperlessUnavailable')<{
35
+ readonly status: number;
36
+ readonly method: string;
37
+ readonly path: string;
38
+ readonly detail: string;
39
+ }> {}
40
+
41
+ export type PaperlessError =
42
+ | PaperlessNotFound
43
+ | PaperlessUnauthorized
44
+ | PaperlessValidation
45
+ | PaperlessUnavailable;
46
+
47
+ /**
48
+ * The one place a status code is read. ⚠️ Anything outside {400,401,403,404,5xx} — 405, 409, 429
49
+ * — is not documented for these four endpoints, so it falls to `PaperlessValidation` rather than
50
+ * inventing a fifth tag for a case that was never measured.
51
+ */
52
+ export const statusToError = (
53
+ status: number,
54
+ method: string,
55
+ path: string,
56
+ body: string,
57
+ ): PaperlessError => {
58
+ if (status === 404) return new PaperlessNotFound({ method, path });
59
+ if (status === 401 || status === 403) return new PaperlessUnauthorized({ method, path, status });
60
+ if (status >= 500) return new PaperlessUnavailable({ detail: body, method, path, status });
61
+ return new PaperlessValidation({ body, method, path });
62
+ };
@@ -0,0 +1,156 @@
1
+ /**
2
+ * A fake Paperless-ngx for the tests beside it — `Bun.serve` on an ephemeral 127.0.0.1 port, the
3
+ * same shape `../openbao/fake-bao.ts` and `../caddy/fake-caddy.ts` use.
4
+ *
5
+ * ⛔ TEST-ONLY. No provider imports this file. It stores rows in memory for exactly the four
6
+ * taxonomy collections and answers `name__iexact` the way Django does — case-insensitive,
7
+ * never collapsing two differently-cased rows into one.
8
+ */
9
+ import * as Effect from 'effect/Effect';
10
+ 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
+
15
+ export const FAKE_TOKEN = 'fake-token-not-real';
16
+
17
+ export interface Seen {
18
+ readonly method: string;
19
+ readonly path: string;
20
+ readonly headers: Headers;
21
+ readonly body: string;
22
+ }
23
+
24
+ export type Row = Record<string, unknown> & { id: number };
25
+
26
+ const COLLECTIONS = ['tags', 'document_types', 'storage_paths', 'custom_fields'] as const;
27
+
28
+ export interface FakePaperless {
29
+ readonly url: string;
30
+ readonly seen: Seen[];
31
+ readonly rows: (collection: (typeof COLLECTIONS)[number]) => readonly Row[];
32
+ /** `undefined` to answer normally; a status to force it once per subsequent call. */
33
+ forceStatus: number | undefined;
34
+ stop(): void;
35
+ }
36
+
37
+ const nextId = (store: Row[]): number =>
38
+ store.length === 0 ? 1 : Math.max(...store.map((r) => r.id)) + 1;
39
+
40
+ export const fakePaperless = (): FakePaperless => {
41
+ const seen: Seen[] = [];
42
+ const store = new Map<(typeof COLLECTIONS)[number], Row[]>(COLLECTIONS.map((c) => [c, []]));
43
+ let forced: number | undefined;
44
+
45
+ const fetch = async (request: Request): Promise<Response> => {
46
+ const url = new URL(request.url);
47
+ const body = await request.text();
48
+ seen.push({
49
+ body,
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.' });
57
+ if (forced !== undefined) {
58
+ const status = forced;
59
+ forced = undefined;
60
+ return json(status, { detail: 'injected for the test' });
61
+ }
62
+
63
+ const segments = url.pathname.split('/').filter((s) => s !== '');
64
+ // ['api', collection, (id)?]
65
+ const collection = segments[1] as (typeof COLLECTIONS)[number] | undefined;
66
+ if (collection === undefined || !COLLECTIONS.includes(collection))
67
+ return json(404, { detail: 'Not found.' });
68
+ const rows = store.get(collection) as Row[];
69
+ const id = segments[2] === undefined ? undefined : Number(segments[2]);
70
+
71
+ if (request.method === 'GET' && id === undefined) {
72
+ const filter = url.searchParams.get('name__iexact');
73
+ const matched =
74
+ filter === null
75
+ ? rows
76
+ : rows.filter((r) => String(r['name']).toLowerCase() === filter.toLowerCase());
77
+ return json(200, { count: matched.length, next: null, previous: null, results: matched });
78
+ }
79
+ if (request.method === 'POST' && id === undefined) {
80
+ const row: Row = { id: nextId(rows), ...(JSON.parse(body) as Record<string, unknown>) };
81
+ rows.push(row);
82
+ return json(201, row);
83
+ }
84
+ const found = id === undefined ? undefined : rows.find((r) => r.id === id);
85
+ if (found === undefined) return json(404, { detail: 'Not found.' });
86
+ if (request.method === 'GET') return json(200, found);
87
+ if (request.method === 'PATCH') {
88
+ Object.assign(found, JSON.parse(body) as Record<string, unknown>);
89
+ return json(200, found);
90
+ }
91
+ if (request.method === 'DELETE') {
92
+ store.set(
93
+ collection,
94
+ rows.filter((r) => r.id !== id),
95
+ );
96
+ return new Response(null, { status: 204 });
97
+ }
98
+ return json(405, { detail: 'method not allowed' });
99
+ };
100
+
101
+ const server = Bun.serve({ fetch, hostname: '127.0.0.1', port: 0 });
102
+ return {
103
+ get forceStatus() {
104
+ return forced;
105
+ },
106
+ set forceStatus(value: number | undefined) {
107
+ forced = value;
108
+ },
109
+ rows: (collection) => store.get(collection) ?? [],
110
+ seen,
111
+ stop: () => void server.stop(true),
112
+ url: server.url.origin,
113
+ };
114
+ };
115
+
116
+ const json = (status: number, body: unknown): Response =>
117
+ new Response(status === 204 ? null : JSON.stringify(body), {
118
+ headers: { 'content-type': 'application/json' },
119
+ status,
120
+ });
121
+
122
+ /** Run `body` against a fresh fake, `PAPERLESS_URL`/`PAPERLESS_TOKEN` set only for its duration. */
123
+ export const withFake = async (body: (fake: FakePaperless) => Promise<void>): Promise<void> => {
124
+ const fake = fakePaperless();
125
+ const savedUrl = process.env['PAPERLESS_URL'];
126
+ const savedToken = process.env['PAPERLESS_TOKEN'];
127
+ process.env['PAPERLESS_URL'] = fake.url;
128
+ process.env['PAPERLESS_TOKEN'] = FAKE_TOKEN;
129
+ try {
130
+ await body(fake);
131
+ } finally {
132
+ fake.stop();
133
+ if (savedUrl === undefined) delete process.env['PAPERLESS_URL'];
134
+ else process.env['PAPERLESS_URL'] = savedUrl;
135
+ if (savedToken === undefined) delete process.env['PAPERLESS_TOKEN'];
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
+ );
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Every generated Paperless constraint table, merged — DO NOT EDIT BY HAND.
3
+ *
4
+ * Run: bun codegen/paperless.ts
5
+ *
6
+ * ⚠️ EVERY KEY IS PREFIXED `paperless:`, so one estate running several vendors' readers cannot
7
+ * collide on an unprefixed `POST /api/…`.
8
+ */
9
+ import type { EndpointConstraints } from '../../constraints.ts';
10
+ import { PAPERLESS_CUSTOM_FIELDS_CONSTRAINTS } from './paperless-custom_fields.ts';
11
+ import { PAPERLESS_DOCUMENT_TYPES_CONSTRAINTS } from './paperless-document_types.ts';
12
+ import { PAPERLESS_STORAGE_PATHS_CONSTRAINTS } from './paperless-storage_paths.ts';
13
+ import { PAPERLESS_TAGS_CONSTRAINTS } from './paperless-tags.ts';
14
+
15
+ export const PAPERLESS_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>> = {
16
+ ...PAPERLESS_CUSTOM_FIELDS_CONSTRAINTS,
17
+ ...PAPERLESS_DOCUMENT_TYPES_CONSTRAINTS,
18
+ ...PAPERLESS_STORAGE_PATHS_CONSTRAINTS,
19
+ ...PAPERLESS_TAGS_CONSTRAINTS,
20
+ };
21
+
22
+ /** sha256 of the merged table, truncated — a digest of the DATA, not the file text. */
23
+ export const PAPERLESS_CONSTRAINTS_DIGEST = '66e61dce8c099d7d';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Generated Paperless-ngx REST API (OpenAPI 3.0.3) request-body constraints for `/api/custom_fields/` — DO NOT EDIT BY HAND.
3
+ *
4
+ * Run: bun codegen/paperless.ts
5
+ * Manifest entry: `paperless-openapi` — Paperless-ngx REST API (OpenAPI 3.0.3) 3.1.1
6
+ * sha256 d0fe550d2135e37b, Paperless-ngx web service, loopback
7
+ * /api/schema/?format=json
8
+ *
9
+ * 8 of this product's 100 POST/PUT/PATCH/DELETE endpoints are tabled across all
10
+ * areas: the ones this package writes to, named in its own source. Every other vendor write
11
+ * endpoint is UNTABLED and therefore unchecked at plan time.
12
+ *
13
+ * ⚠️ A `patternSource` with no `pattern` beside it could NOT be carried into a JavaScript
14
+ * RegExp faithfully — Django/DRF's `\w` is Unicode and JavaScript's is ASCII
15
+ * (codegen/py-pattern.ts). It is recorded and NOT enforced.
16
+ * ⚠️ `data_type` on a custom field is CREATE-ONLY: enforced here as a value rule, never as a
17
+ * reason to allow a PATCH — the refusal on change lives in the provider (custom-field.ts).
18
+ */
19
+ import type { EndpointConstraints } from '../../constraints.ts';
20
+
21
+ export const PAPERLESS_CUSTOM_FIELDS_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>> = {
22
+ "paperless:PATCH /api/custom_fields/{id}/": {
23
+ "name": {"maxLength":128,"minLength":1,"type":"string"},
24
+ },
25
+ "paperless:POST /api/custom_fields/": {
26
+ "data_type": {"required":true},
27
+ "name": {"maxLength":128,"minLength":1,"required":true,"type":"string"},
28
+ },
29
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Generated Paperless-ngx REST API (OpenAPI 3.0.3) request-body constraints for `/api/document_types/` — DO NOT EDIT BY HAND.
3
+ *
4
+ * Run: bun codegen/paperless.ts
5
+ * Manifest entry: `paperless-openapi` — Paperless-ngx REST API (OpenAPI 3.0.3) 3.1.1
6
+ * sha256 d0fe550d2135e37b, Paperless-ngx web service, loopback
7
+ * /api/schema/?format=json
8
+ *
9
+ * 8 of this product's 100 POST/PUT/PATCH/DELETE endpoints are tabled across all
10
+ * areas: the ones this package writes to, named in its own source. Every other vendor write
11
+ * endpoint is UNTABLED and therefore unchecked at plan time.
12
+ *
13
+ * ⚠️ A `patternSource` with no `pattern` beside it could NOT be carried into a JavaScript
14
+ * RegExp faithfully — Django/DRF's `\w` is Unicode and JavaScript's is ASCII
15
+ * (codegen/py-pattern.ts). It is recorded and NOT enforced.
16
+ * ⚠️ `data_type` on a custom field is CREATE-ONLY: enforced here as a value rule, never as a
17
+ * reason to allow a PATCH — the refusal on change lives in the provider (custom-field.ts).
18
+ */
19
+ import type { EndpointConstraints } from '../../constraints.ts';
20
+
21
+ export const PAPERLESS_DOCUMENT_TYPES_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>> = {
22
+ "paperless:PATCH /api/document_types/{id}/": {
23
+ "match": {"maxLength":256,"type":"string"},
24
+ "matching_algorithm": {"enum":[0,1,2,3,4,5,6],"type":"integer"},
25
+ "name": {"maxLength":128,"minLength":1,"type":"string"},
26
+ },
27
+ "paperless:POST /api/document_types/": {
28
+ "match": {"maxLength":256,"type":"string"},
29
+ "matching_algorithm": {"enum":[0,1,2,3,4,5,6],"type":"integer"},
30
+ "name": {"maxLength":128,"minLength":1,"required":true,"type":"string"},
31
+ },
32
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Generated Paperless-ngx REST API (OpenAPI 3.0.3) request-body constraints for `/api/storage_paths/` — DO NOT EDIT BY HAND.
3
+ *
4
+ * Run: bun codegen/paperless.ts
5
+ * Manifest entry: `paperless-openapi` — Paperless-ngx REST API (OpenAPI 3.0.3) 3.1.1
6
+ * sha256 d0fe550d2135e37b, Paperless-ngx web service, loopback
7
+ * /api/schema/?format=json
8
+ *
9
+ * 8 of this product's 100 POST/PUT/PATCH/DELETE endpoints are tabled across all
10
+ * areas: the ones this package writes to, named in its own source. Every other vendor write
11
+ * endpoint is UNTABLED and therefore unchecked at plan time.
12
+ *
13
+ * ⚠️ A `patternSource` with no `pattern` beside it could NOT be carried into a JavaScript
14
+ * RegExp faithfully — Django/DRF's `\w` is Unicode and JavaScript's is ASCII
15
+ * (codegen/py-pattern.ts). It is recorded and NOT enforced.
16
+ * ⚠️ `data_type` on a custom field is CREATE-ONLY: enforced here as a value rule, never as a
17
+ * reason to allow a PATCH — the refusal on change lives in the provider (custom-field.ts).
18
+ */
19
+ import type { EndpointConstraints } from '../../constraints.ts';
20
+
21
+ export const PAPERLESS_STORAGE_PATHS_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>> = {
22
+ "paperless:PATCH /api/storage_paths/{id}/": {
23
+ "match": {"maxLength":256,"type":"string"},
24
+ "matching_algorithm": {"enum":[0,1,2,3,4,5,6],"type":"integer"},
25
+ "name": {"maxLength":128,"minLength":1,"type":"string"},
26
+ "path": {"minLength":1,"type":"string"},
27
+ },
28
+ "paperless:POST /api/storage_paths/": {
29
+ "match": {"maxLength":256,"type":"string"},
30
+ "matching_algorithm": {"enum":[0,1,2,3,4,5,6],"type":"integer"},
31
+ "name": {"maxLength":128,"minLength":1,"required":true,"type":"string"},
32
+ "path": {"minLength":1,"required":true,"type":"string"},
33
+ },
34
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Generated Paperless-ngx REST API (OpenAPI 3.0.3) request-body constraints for `/api/tags/` — DO NOT EDIT BY HAND.
3
+ *
4
+ * Run: bun codegen/paperless.ts
5
+ * Manifest entry: `paperless-openapi` — Paperless-ngx REST API (OpenAPI 3.0.3) 3.1.1
6
+ * sha256 d0fe550d2135e37b, Paperless-ngx web service, loopback
7
+ * /api/schema/?format=json
8
+ *
9
+ * 8 of this product's 100 POST/PUT/PATCH/DELETE endpoints are tabled across all
10
+ * areas: the ones this package writes to, named in its own source. Every other vendor write
11
+ * endpoint is UNTABLED and therefore unchecked at plan time.
12
+ *
13
+ * ⚠️ A `patternSource` with no `pattern` beside it could NOT be carried into a JavaScript
14
+ * RegExp faithfully — Django/DRF's `\w` is Unicode and JavaScript's is ASCII
15
+ * (codegen/py-pattern.ts). It is recorded and NOT enforced.
16
+ * ⚠️ `data_type` on a custom field is CREATE-ONLY: enforced here as a value rule, never as a
17
+ * reason to allow a PATCH — the refusal on change lives in the provider (custom-field.ts).
18
+ */
19
+ import type { EndpointConstraints } from '../../constraints.ts';
20
+
21
+ export const PAPERLESS_TAGS_CONSTRAINTS: Readonly<Record<string, EndpointConstraints>> = {
22
+ "paperless:PATCH /api/tags/{id}/": {
23
+ "color": {"maxLength":7,"minLength":1,"type":"string"},
24
+ "match": {"maxLength":256,"type":"string"},
25
+ "matching_algorithm": {"enum":[0,1,2,3,4,5,6],"type":"integer"},
26
+ "name": {"maxLength":128,"minLength":1,"type":"string"},
27
+ },
28
+ "paperless:POST /api/tags/": {
29
+ "color": {"maxLength":7,"minLength":1,"type":"string"},
30
+ "match": {"maxLength":256,"type":"string"},
31
+ "matching_algorithm": {"enum":[0,1,2,3,4,5,6],"type":"integer"},
32
+ "name": {"maxLength":128,"minLength":1,"required":true,"type":"string"},
33
+ },
34
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Generated Paperless-ngx REST API (OpenAPI 3.0.3) request/response types for `/api/custom_fields/` — DO NOT EDIT BY HAND.
3
+ *
4
+ * Run: bun codegen/paperless.ts
5
+ * Manifest entry: `paperless-openapi` — Paperless-ngx REST API (OpenAPI 3.0.3) 3.1.1, sha256 d0fe550d2135e37b
6
+ *
7
+ * 8 of Paperless-ngx REST API (OpenAPI 3.0.3)'s 100 write endpoints are used by this family; this file covers only /api/custom_fields/.
8
+ *
9
+ * ⛔ `set_permissions` (writeOnly) is not modelled in this slice — no property here writes it.
10
+ */
11
+ export interface PaperlessCustomFieldRequest {
12
+ name: string;
13
+ data_type: "string" | "url" | "date" | "boolean" | "integer" | "float" | "monetary" | "documentlink" | "select" | "longtext";
14
+ extra_data?: unknown | null;
15
+ }
16
+
17
+ export interface PaperlessCustomField {
18
+ readonly id: number;
19
+ name: string;
20
+ data_type: "string" | "url" | "date" | "boolean" | "integer" | "float" | "monetary" | "documentlink" | "select" | "longtext";
21
+ extra_data?: unknown | null;
22
+ readonly document_count: number;
23
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Generated Paperless-ngx REST API (OpenAPI 3.0.3) request/response types for `/api/document_types/` — DO NOT EDIT BY HAND.
3
+ *
4
+ * Run: bun codegen/paperless.ts
5
+ * Manifest entry: `paperless-openapi` — Paperless-ngx REST API (OpenAPI 3.0.3) 3.1.1, sha256 d0fe550d2135e37b
6
+ *
7
+ * 8 of Paperless-ngx REST API (OpenAPI 3.0.3)'s 100 write endpoints are used by this family; this file covers only /api/document_types/.
8
+ *
9
+ * ⛔ `set_permissions` (writeOnly) is not modelled in this slice — no property here writes it.
10
+ */
11
+ export interface PaperlessDocumentTypeRequest {
12
+ name: string;
13
+ match?: string;
14
+ matching_algorithm?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
15
+ is_insensitive?: boolean;
16
+ owner?: number | null;
17
+ }
18
+
19
+ export interface PaperlessDocumentType {
20
+ readonly id: number;
21
+ readonly slug: string;
22
+ name: string;
23
+ match?: string;
24
+ matching_algorithm?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
25
+ is_insensitive?: boolean;
26
+ readonly document_count: number;
27
+ owner?: number | null;
28
+ readonly permissions: { view?: { users?: number[]; groups?: number[] }; change?: { users?: number[]; groups?: number[] } };
29
+ readonly user_can_change: boolean;
30
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Every generated Paperless taxonomy type, re-exported — DO NOT EDIT BY HAND.
3
+ *
4
+ * Run: bun codegen/paperless.ts
5
+ */
6
+ export * from './tags.ts';
7
+ export * from './document-types.ts';
8
+ export * from './storage-paths.ts';
9
+ export * from './custom-fields.ts';
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Generated Paperless-ngx REST API (OpenAPI 3.0.3) request/response types for `/api/storage_paths/` — DO NOT EDIT BY HAND.
3
+ *
4
+ * Run: bun codegen/paperless.ts
5
+ * Manifest entry: `paperless-openapi` — Paperless-ngx REST API (OpenAPI 3.0.3) 3.1.1, sha256 d0fe550d2135e37b
6
+ *
7
+ * 8 of Paperless-ngx REST API (OpenAPI 3.0.3)'s 100 write endpoints are used by this family; this file covers only /api/storage_paths/.
8
+ *
9
+ * ⛔ `set_permissions` (writeOnly) is not modelled in this slice — no property here writes it.
10
+ */
11
+ export interface PaperlessStoragePathRequest {
12
+ name: string;
13
+ path: string;
14
+ match?: string;
15
+ matching_algorithm?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
16
+ is_insensitive?: boolean;
17
+ owner?: number | null;
18
+ }
19
+
20
+ export interface PaperlessStoragePath {
21
+ readonly id: number;
22
+ readonly slug: string;
23
+ name: string;
24
+ path: string;
25
+ match?: string;
26
+ matching_algorithm?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
27
+ is_insensitive?: boolean;
28
+ readonly document_count: number;
29
+ owner?: number | null;
30
+ readonly user_can_change: boolean;
31
+ }