@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.
- package/README.md +25 -1
- package/dist/litellm/client.d.ts +76 -0
- package/dist/litellm/client.d.ts.map +1 -0
- package/dist/litellm/credentials.d.ts +48 -0
- package/dist/litellm/credentials.d.ts.map +1 -0
- package/dist/litellm/fake-litellm.d.ts +36 -0
- package/dist/litellm/fake-litellm.d.ts.map +1 -0
- package/dist/litellm/fake-stack.d.ts +30 -0
- package/dist/litellm/fake-stack.d.ts.map +1 -0
- package/dist/litellm/generated/pass-through.d.ts +71 -0
- package/dist/litellm/generated/pass-through.d.ts.map +1 -0
- package/dist/litellm/index.d.ts +13 -0
- package/dist/litellm/index.d.ts.map +1 -0
- package/dist/litellm/index.js +270 -0
- package/dist/litellm/index.js.map +14 -0
- package/dist/litellm/pass-through-endpoint.d.ts +94 -0
- package/dist/litellm/pass-through-endpoint.d.ts.map +1 -0
- package/dist/litellm/pass-through-form.d.ts +40 -0
- package/dist/litellm/pass-through-form.d.ts.map +1 -0
- package/dist/litellm/providers.d.ts +16 -0
- package/dist/litellm/providers.d.ts.map +1 -0
- package/dist/paperless/client.d.ts +37 -0
- package/dist/paperless/client.d.ts.map +1 -0
- package/dist/paperless/constraint-guard.d.ts +19 -0
- package/dist/paperless/constraint-guard.d.ts.map +1 -0
- package/dist/paperless/constraints.d.ts +50 -0
- package/dist/paperless/constraints.d.ts.map +1 -0
- package/dist/paperless/credentials.d.ts +40 -0
- package/dist/paperless/credentials.d.ts.map +1 -0
- package/dist/paperless/custom-field.d.ts +75 -0
- package/dist/paperless/custom-field.d.ts.map +1 -0
- package/dist/paperless/document-type.d.ts +30 -0
- package/dist/paperless/document-type.d.ts.map +1 -0
- package/dist/paperless/errors.d.ts +49 -0
- package/dist/paperless/errors.d.ts.map +1 -0
- package/dist/paperless/fake-paperless.d.ts +39 -0
- package/dist/paperless/fake-paperless.d.ts.map +1 -0
- package/dist/paperless/generated/constraints/index.d.ts +13 -0
- package/dist/paperless/generated/constraints/index.d.ts.map +1 -0
- package/dist/paperless/generated/constraints/paperless-custom_fields.d.ts +21 -0
- package/dist/paperless/generated/constraints/paperless-custom_fields.d.ts.map +1 -0
- package/dist/paperless/generated/constraints/paperless-document_types.d.ts +21 -0
- package/dist/paperless/generated/constraints/paperless-document_types.d.ts.map +1 -0
- package/dist/paperless/generated/constraints/paperless-storage_paths.d.ts +21 -0
- package/dist/paperless/generated/constraints/paperless-storage_paths.d.ts.map +1 -0
- package/dist/paperless/generated/constraints/paperless-tags.d.ts +21 -0
- package/dist/paperless/generated/constraints/paperless-tags.d.ts.map +1 -0
- package/dist/paperless/generated/types/custom-fields.d.ts +23 -0
- package/dist/paperless/generated/types/custom-fields.d.ts.map +1 -0
- package/dist/paperless/generated/types/document-types.d.ts +39 -0
- package/dist/paperless/generated/types/document-types.d.ts.map +1 -0
- package/dist/paperless/generated/types/index.d.ts +10 -0
- package/dist/paperless/generated/types/index.d.ts.map +1 -0
- package/dist/paperless/generated/types/storage-paths.d.ts +31 -0
- package/dist/paperless/generated/types/storage-paths.d.ts.map +1 -0
- package/dist/paperless/generated/types/tags.d.ts +37 -0
- package/dist/paperless/generated/types/tags.d.ts.map +1 -0
- package/dist/paperless/index.d.ts +19 -0
- package/dist/paperless/index.d.ts.map +1 -0
- package/dist/paperless/index.js +524 -0
- package/dist/paperless/index.js.map +26 -0
- package/dist/paperless/matching-locate.d.ts +33 -0
- package/dist/paperless/matching-locate.d.ts.map +1 -0
- package/dist/paperless/matching-types.d.ts +60 -0
- package/dist/paperless/matching-types.d.ts.map +1 -0
- package/dist/paperless/matching.d.ts +61 -0
- package/dist/paperless/matching.d.ts.map +1 -0
- package/dist/paperless/providers.d.ts +15 -0
- package/dist/paperless/providers.d.ts.map +1 -0
- package/dist/paperless/storage-path.d.ts +32 -0
- package/dist/paperless/storage-path.d.ts.map +1 -0
- package/dist/paperless/tag.d.ts +47 -0
- package/dist/paperless/tag.d.ts.map +1 -0
- package/dist/postgres/connection.d.ts +50 -0
- package/dist/postgres/connection.d.ts.map +1 -0
- package/dist/postgres/database-attrs.d.ts +79 -0
- package/dist/postgres/database-attrs.d.ts.map +1 -0
- package/dist/postgres/database-sql.d.ts +73 -0
- package/dist/postgres/database-sql.d.ts.map +1 -0
- package/dist/postgres/database.d.ts +63 -0
- package/dist/postgres/database.d.ts.map +1 -0
- package/dist/postgres/errors.d.ts +86 -0
- package/dist/postgres/errors.d.ts.map +1 -0
- package/dist/postgres/fake-sql.d.ts +22 -0
- package/dist/postgres/fake-sql.d.ts.map +1 -0
- package/dist/postgres/index.d.ts +16 -0
- package/dist/postgres/index.d.ts.map +1 -0
- package/dist/postgres/index.js +238 -0
- package/dist/postgres/index.js.map +15 -0
- package/dist/postgres/providers.d.ts +14 -0
- package/dist/postgres/providers.d.ts.map +1 -0
- package/dist/proxmox/index.js.map +2 -2
- package/dist/proxmox/zfs-pool.d.ts +10 -2
- package/dist/proxmox/zfs-pool.d.ts.map +1 -1
- package/dist/release/archive.d.ts +6 -2
- package/dist/release/archive.d.ts.map +1 -1
- package/dist/release/binary-form.d.ts +14 -9
- package/dist/release/binary-form.d.ts.map +1 -1
- package/dist/release/binary-pins.d.ts +17 -0
- package/dist/release/binary-pins.d.ts.map +1 -0
- package/dist/release/catalog.d.ts.map +1 -1
- package/dist/release/index.js +90 -40
- package/dist/release/index.js.map +8 -7
- package/dist/release/tar.d.ts +9 -2
- package/dist/release/tar.d.ts.map +1 -1
- package/docs/litellm.md +110 -0
- package/docs/paperless.md +115 -0
- package/docs/peers.md +8 -1
- package/docs/postgres.md +125 -0
- package/docs/release-binary-catalogs.md +40 -4
- package/docs/release-binary-refusals.md +22 -8
- package/docs/release-binary-upstream.md +33 -15
- package/package.json +14 -1
- package/src/litellm/client.ts +195 -0
- package/src/litellm/credentials.ts +73 -0
- package/src/litellm/fake-litellm.ts +114 -0
- package/src/litellm/fake-stack.ts +100 -0
- package/src/litellm/generated/pass-through.ts +73 -0
- package/src/litellm/index.ts +42 -0
- package/src/litellm/pass-through-endpoint-delete-swallow.test.ts +66 -0
- package/src/litellm/pass-through-endpoint-replace.test.ts +84 -0
- package/src/litellm/pass-through-endpoint.test.ts +208 -0
- package/src/litellm/pass-through-endpoint.ts +258 -0
- package/src/litellm/pass-through-form.test.ts +128 -0
- package/src/litellm/pass-through-form.ts +136 -0
- package/src/litellm/providers.ts +19 -0
- package/src/paperless/client.ts +88 -0
- package/src/paperless/constraint-guard.ts +50 -0
- package/src/paperless/constraints.test.ts +74 -0
- package/src/paperless/constraints.ts +111 -0
- package/src/paperless/credentials.ts +69 -0
- package/src/paperless/custom-field.test.ts +74 -0
- package/src/paperless/custom-field.ts +95 -0
- package/src/paperless/document-type.ts +74 -0
- package/src/paperless/errors.ts +62 -0
- package/src/paperless/fake-paperless.ts +156 -0
- package/src/paperless/generated/constraints/index.ts +23 -0
- package/src/paperless/generated/constraints/paperless-custom_fields.ts +29 -0
- package/src/paperless/generated/constraints/paperless-document_types.ts +32 -0
- package/src/paperless/generated/constraints/paperless-storage_paths.ts +34 -0
- package/src/paperless/generated/constraints/paperless-tags.ts +34 -0
- package/src/paperless/generated/types/custom-fields.ts +23 -0
- package/src/paperless/generated/types/document-types.ts +30 -0
- package/src/paperless/generated/types/index.ts +9 -0
- package/src/paperless/generated/types/storage-paths.ts +31 -0
- package/src/paperless/generated/types/tags.ts +37 -0
- package/src/paperless/index.ts +56 -0
- package/src/paperless/matching-identity.test.ts +194 -0
- package/src/paperless/matching-locate.ts +104 -0
- package/src/paperless/matching-types.ts +61 -0
- package/src/paperless/matching.test.ts +128 -0
- package/src/paperless/matching.ts +239 -0
- package/src/paperless/providers.ts +27 -0
- package/src/paperless/storage-path.ts +78 -0
- package/src/paperless/tag.ts +91 -0
- package/src/postgres/connection.ts +62 -0
- package/src/postgres/database-attrs.ts +87 -0
- package/src/postgres/database-bigint-serialization.test.ts +140 -0
- package/src/postgres/database-lifecycle.test.ts +113 -0
- package/src/postgres/database-provenance.test.ts +118 -0
- package/src/postgres/database-refusals.test.ts +135 -0
- package/src/postgres/database-sql.ts +174 -0
- package/src/postgres/database.ts +193 -0
- package/src/postgres/errors.ts +131 -0
- package/src/postgres/fake-sql.ts +123 -0
- package/src/postgres/fixtures/pg-REL_18_6-create-database-synopsis.txt +26 -0
- package/src/postgres/fixtures/pg-REL_18_6-pg_database.h.txt +67 -0
- package/src/postgres/index.ts +30 -0
- package/src/postgres/providers.ts +16 -0
- package/src/proxmox/zfs-pool.ts +10 -2
- package/src/release/archive.ts +10 -4
- package/src/release/binary-form.ts +24 -76
- package/src/release/binary-pins.ts +95 -0
- package/src/release/binary-root.test.ts +122 -0
- package/src/release/catalog.ts +10 -2
- package/src/release/tar-root.test.ts +114 -0
- package/src/release/tar.ts +78 -12
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One shape for every Paperless taxonomy object (Tag, DocumentType, StoragePath, CustomField):
|
|
3
|
+
* locate by `name__iexact` only while there is no prior state, identify the exact (name, owner),
|
|
4
|
+
* create-or-update, never delete by default. Modelled on `../netbox/resource.ts`'s
|
|
5
|
+
* `netboxOperations`, with the differences this unit's spec calls for.
|
|
6
|
+
*
|
|
7
|
+
* ⛔ THE CREATE BODY ALWAYS CARRIES `owner`, NULL WHEN UNDECLARED. `OwnedObjectSerializer.create`
|
|
8
|
+
* sets the owner to the TOKEN USER when the field is absent from the body (Paperless-ngx
|
|
9
|
+
* `documents/serialisers.py:495-502` at v3.1.1) — omitting it the way NetBox's `prefix-form.ts`
|
|
10
|
+
* omits an undeclared foreign key would silently create an object owned by whichever credential
|
|
11
|
+
* ran the deploy, invisible to every other user and a second row beside the estate's `owner:
|
|
12
|
+
* null` one on the next apply. Sending `owner: null` explicitly is what keeps a create
|
|
13
|
+
* idempotent under this vendor's default.
|
|
14
|
+
*
|
|
15
|
+
* ★ READ ANSWERS `Unowned(attrs)` ON EVERY MATCH (H1, alchemy-provider-standard). Paperless has no
|
|
16
|
+
* ownership marker this package could check — `owner` here is the estate's OWN prop, not proof
|
|
17
|
+
* 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. `--adopt` (or `adopt(true)`) resolves it, same as any other
|
|
19
|
+
* marker-less API (upstream's `Snippet.ts#read`).
|
|
20
|
+
*
|
|
21
|
+
* ⛔ IDENTITY IS THE STORED `id`, NOT `name`/`owner`, ONCE A RESOURCE HAS STATE (PR 163, red-team
|
|
22
|
+
* HIGH finding). `diff` and `reconcile` used to re-derive identity from `news` alone on EVERY
|
|
23
|
+
* call — `fetchLive` located a live row by `(name, owner)` even when `output` (the engine's
|
|
24
|
+
* record of what this declaration last wrote, `Provider.ts#reconcile`'s `output` parameter) was
|
|
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`.
|
|
47
|
+
*/
|
|
48
|
+
import { Unowned } from 'alchemy/AdoptPolicy';
|
|
49
|
+
import { isResolved } from 'alchemy/Diff';
|
|
50
|
+
import type { Input } from 'alchemy/Input';
|
|
51
|
+
import * as Effect from 'effect/Effect';
|
|
52
|
+
import type { PaperlessRow } from './client.ts';
|
|
53
|
+
import { paperless } from './client.ts';
|
|
54
|
+
import { guardBody } from './constraint-guard.ts';
|
|
55
|
+
import type { PaperlessBody } from './constraints.ts';
|
|
56
|
+
import { absentOn404, matchingLocate, ownerOf } from './matching-locate.ts';
|
|
57
|
+
import type { MatchingProps, MatchingSpec } from './matching-types.ts';
|
|
58
|
+
|
|
59
|
+
/** 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
|
+
export type {
|
|
61
|
+
FieldSpec,
|
|
62
|
+
MatchingError,
|
|
63
|
+
MatchingProps,
|
|
64
|
+
MatchingRequirements,
|
|
65
|
+
MatchingSpec,
|
|
66
|
+
} from './matching-types.ts';
|
|
67
|
+
|
|
68
|
+
/** Object equality good enough for this family's scalars, nullable ints and `extra_data` JSON. */
|
|
69
|
+
const sameValue = (a: unknown, b: unknown): boolean =>
|
|
70
|
+
typeof a === 'object' && a !== null ? JSON.stringify(a) === JSON.stringify(b) : Object.is(a, b);
|
|
71
|
+
|
|
72
|
+
export const matchingOperations = <
|
|
73
|
+
Props extends MatchingProps,
|
|
74
|
+
Attributes extends { readonly id: number },
|
|
75
|
+
>(
|
|
76
|
+
spec: MatchingSpec<Props, Attributes>,
|
|
77
|
+
) => {
|
|
78
|
+
// ★ IDENTITY RESOLUTION LIVES IN `matching-locate.ts` (file-size cap) — `fetchByName` for the
|
|
79
|
+
// no-state path, `fetchLive` for the `output.id`-first switch this file's header describes.
|
|
80
|
+
const { fetchByName, fetchById, fetchLive, objectPath } = matchingLocate<Props>(spec);
|
|
81
|
+
|
|
82
|
+
/** ⚠️ ALWAYS NAME-BASED — this is `read`'s own contract (S7): find the object from props alone, with no `output` to consult. Unaffected by the identity switch above. */
|
|
83
|
+
const read = (props: Props) =>
|
|
84
|
+
fetchByName(props).pipe(
|
|
85
|
+
Effect.map((live) => (live === undefined ? undefined : spec.attributes(live, props))),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const readById = (props: Props, id: number) =>
|
|
89
|
+
fetchById(id).pipe(
|
|
90
|
+
Effect.map((live) => (live === undefined ? undefined : spec.attributes(live, props))),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const createBody = (props: Props): PaperlessBody => {
|
|
94
|
+
const out: PaperlessBody =
|
|
95
|
+
spec.owned === false
|
|
96
|
+
? { name: props.name }
|
|
97
|
+
: { name: props.name, owner: props.owner ?? null };
|
|
98
|
+
for (const field of [...spec.fields, ...(spec.createOnly ?? [])]) {
|
|
99
|
+
const value = field.prop(props);
|
|
100
|
+
if (value !== undefined) out[field.key] = value;
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* ⚠️ Only fields that DIFFER from the live row — a PATCH that repeats an unchanged value is
|
|
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 => {
|
|
114
|
+
const out: PaperlessBody = {};
|
|
115
|
+
if (props.name !== live['name']) out['name'] = props.name;
|
|
116
|
+
if (spec.owned !== false && ownerOf(live) !== (props.owner ?? undefined)) {
|
|
117
|
+
out['owner'] = props.owner ?? null;
|
|
118
|
+
}
|
|
119
|
+
for (const field of spec.fields) {
|
|
120
|
+
const value = field.prop(props);
|
|
121
|
+
if (value !== undefined && !sameValue(value, field.live(live))) out[field.key] = value;
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const matches = (props: Props, live: PaperlessRow): boolean => {
|
|
127
|
+
if (props.name !== live['name']) return false;
|
|
128
|
+
if (spec.owned !== false && ownerOf(live) !== (props.owner ?? undefined)) return false;
|
|
129
|
+
return spec.fields.every((field) => {
|
|
130
|
+
const value = field.prop(props);
|
|
131
|
+
return value === undefined || sameValue(value, field.live(live));
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
read,
|
|
137
|
+
|
|
138
|
+
diff: (news: Input<Props>, output: Attributes | undefined) =>
|
|
139
|
+
Effect.gen(function* () {
|
|
140
|
+
if (output === undefined || !isResolved(news)) return undefined;
|
|
141
|
+
const live = yield* fetchLive(news, output);
|
|
142
|
+
if (live === undefined) return { action: 'update' } as const;
|
|
143
|
+
const refusal = spec.immutable?.(live, news);
|
|
144
|
+
if (refusal !== undefined) return yield* Effect.die(new Error(refusal));
|
|
145
|
+
if (matches(news, live)) return { action: 'noop' } as const;
|
|
146
|
+
return spec.endpoint.update === undefined
|
|
147
|
+
? ({ action: 'replace' } as const)
|
|
148
|
+
: ({ action: 'update' } as const);
|
|
149
|
+
}),
|
|
150
|
+
|
|
151
|
+
reconcile: (news: Props, output: Attributes | undefined) =>
|
|
152
|
+
Effect.gen(function* () {
|
|
153
|
+
const before = yield* fetchLive(news, output);
|
|
154
|
+
let id: number | undefined;
|
|
155
|
+
if (before === undefined) {
|
|
156
|
+
const body = createBody(news);
|
|
157
|
+
yield* guardBody(spec.endpoint.create, body, true);
|
|
158
|
+
yield* paperless('POST', `${spec.collection}/`, body);
|
|
159
|
+
} else {
|
|
160
|
+
id = typeof before['id'] === 'number' ? before['id'] : undefined;
|
|
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.
|
|
163
|
+
const refusal = spec.immutable?.(before, news);
|
|
164
|
+
if (refusal !== undefined) return yield* Effect.die(new Error(refusal));
|
|
165
|
+
if (spec.endpoint.update !== undefined) {
|
|
166
|
+
const body = patchBody(news, before);
|
|
167
|
+
if (Object.keys(body).length > 0) {
|
|
168
|
+
yield* guardBody(spec.endpoint.update, body, false);
|
|
169
|
+
yield* paperless('PATCH', `${objectPath(before)}/`, body);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
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
|
+
const after = yield* id === undefined ? read(news) : readById(news, id);
|
|
178
|
+
if (after === undefined) {
|
|
179
|
+
return yield* Effect.die(
|
|
180
|
+
new Error(
|
|
181
|
+
`${spec.describe(news)}: the write returned no error but the object is still absent.`,
|
|
182
|
+
),
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return after;
|
|
186
|
+
}),
|
|
187
|
+
|
|
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
|
+
*/
|
|
201
|
+
destroy: (olds: Props, output: Attributes | undefined) =>
|
|
202
|
+
Effect.gen(function* () {
|
|
203
|
+
const live = output === undefined ? yield* fetchByName(olds) : yield* fetchById(output.id);
|
|
204
|
+
if (live === undefined) return;
|
|
205
|
+
yield* absentOn404(paperless('DELETE', `${objectPath(live)}/`));
|
|
206
|
+
}),
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/** The five provider handlers for a spec'd Paperless taxonomy object, wired once. */
|
|
211
|
+
export const matchingHandlers = <
|
|
212
|
+
Props extends MatchingProps,
|
|
213
|
+
Attributes extends { readonly id: number },
|
|
214
|
+
>(
|
|
215
|
+
spec: MatchingSpec<Props, Attributes>,
|
|
216
|
+
) => {
|
|
217
|
+
const ops = matchingOperations(spec);
|
|
218
|
+
return {
|
|
219
|
+
/** ⚠️ `[]` — Paperless index endpoints return the whole estate; adoption stays explicit. */
|
|
220
|
+
list: () => Effect.succeed([]),
|
|
221
|
+
read: ({ olds }: { olds: Props }) =>
|
|
222
|
+
ops
|
|
223
|
+
.read(olds)
|
|
224
|
+
.pipe(Effect.map((attrs) => (attrs === undefined ? undefined : Unowned(attrs)))),
|
|
225
|
+
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.
|
|
232
|
+
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.
|
|
236
|
+
delete: ({ olds, output }: { olds: Props; output?: Attributes | undefined }) =>
|
|
237
|
+
ops.destroy(olds, output),
|
|
238
|
+
};
|
|
239
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All four Paperless taxonomy providers, one credentials layer shared between them.
|
|
3
|
+
*
|
|
4
|
+
* Layer.mergeAll(paperlessProviders(), …the stack's other providers)
|
|
5
|
+
*
|
|
6
|
+
* ⚠️ `HttpClient.HttpClient` IS NOT BUNDLED HERE, and that follows `../netbox/index.ts` and
|
|
7
|
+
* `../forgejo/client.ts`'s own precedent (its header: "the idiom Alchemy's own providers
|
|
8
|
+
* follow — `Hetzner/Providers.ts` builds on `FetchHttpClient.layer`") rather than diverging
|
|
9
|
+
* from it: the transport is the consuming STACK's choice (`FetchHttpClient.layer` in
|
|
10
|
+
* production, a fake `HttpClient` in a test), not a house default a provider file should pin.
|
|
11
|
+
*/
|
|
12
|
+
import * as Layer from 'effect/Layer';
|
|
13
|
+
import { CustomFieldProvider } from './custom-field.ts';
|
|
14
|
+
import { DocumentTypeProvider } from './document-type.ts';
|
|
15
|
+
import { type PaperlessCredentials, environmentLayer } from './credentials.ts';
|
|
16
|
+
import { StoragePathProvider } from './storage-path.ts';
|
|
17
|
+
import { TagProvider } from './tag.ts';
|
|
18
|
+
|
|
19
|
+
export const paperlessProviders = (
|
|
20
|
+
credentials: Layer.Layer<PaperlessCredentials> = environmentLayer,
|
|
21
|
+
) =>
|
|
22
|
+
Layer.mergeAll(
|
|
23
|
+
TagProvider(),
|
|
24
|
+
DocumentTypeProvider(),
|
|
25
|
+
StoragePathProvider(),
|
|
26
|
+
CustomFieldProvider(),
|
|
27
|
+
).pipe(Layer.provide(credentials));
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
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.
|
|
4
|
+
*/
|
|
5
|
+
import { Resource } from 'alchemy';
|
|
6
|
+
import * as Provider from 'alchemy/Provider';
|
|
7
|
+
import * as Effect from 'effect/Effect';
|
|
8
|
+
import type { PaperlessStoragePathRequest } from './generated/types/storage-paths.ts';
|
|
9
|
+
import { type MatchingProps, type MatchingRequirements, matchingHandlers } from './matching.ts';
|
|
10
|
+
|
|
11
|
+
export interface StoragePathProps extends MatchingProps {
|
|
12
|
+
/** ⚠️ maxLength 128 (generated constraint table). The locate key. */
|
|
13
|
+
name: string;
|
|
14
|
+
/** ⚠️ minLength 1 (generated table). Paperless's storage-path template syntax, e.g. `{correspondent}/{title}`. */
|
|
15
|
+
path: string;
|
|
16
|
+
/** ⚠️ maxLength 256. */
|
|
17
|
+
match?: string;
|
|
18
|
+
matchingAlgorithm?: PaperlessStoragePathRequest['matching_algorithm'];
|
|
19
|
+
isInsensitive?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface StoragePathAttributes {
|
|
23
|
+
id: number;
|
|
24
|
+
name: string;
|
|
25
|
+
path: string;
|
|
26
|
+
match: string;
|
|
27
|
+
matchingAlgorithm: number;
|
|
28
|
+
isInsensitive: boolean;
|
|
29
|
+
owner: number | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface StoragePath extends Resource<
|
|
33
|
+
'Paperless.StoragePath',
|
|
34
|
+
StoragePathProps,
|
|
35
|
+
StoragePathAttributes,
|
|
36
|
+
never,
|
|
37
|
+
MatchingRequirements
|
|
38
|
+
> {}
|
|
39
|
+
|
|
40
|
+
export const StoragePath = Resource<StoragePath>('Paperless.StoragePath', {
|
|
41
|
+
defaultRemovalPolicy: 'retain',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const handlers = matchingHandlers<StoragePathProps, StoragePathAttributes>({
|
|
45
|
+
attributes: (live, props) => {
|
|
46
|
+
const id = live['id'];
|
|
47
|
+
if (typeof id !== 'number') return undefined;
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
isInsensitive: live['is_insensitive'] === true,
|
|
51
|
+
match: typeof live['match'] === 'string' ? live['match'] : '',
|
|
52
|
+
matchingAlgorithm:
|
|
53
|
+
typeof live['matching_algorithm'] === 'number' ? live['matching_algorithm'] : 0,
|
|
54
|
+
name: typeof live['name'] === 'string' ? live['name'] : props.name,
|
|
55
|
+
owner: typeof live['owner'] === 'number' ? live['owner'] : undefined,
|
|
56
|
+
path: typeof live['path'] === 'string' ? live['path'] : props.path,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
collection: 'storage_paths',
|
|
60
|
+
describe: (props) => `storage_paths ${props.name}`,
|
|
61
|
+
endpoint: {
|
|
62
|
+
create: 'paperless:POST /api/storage_paths/',
|
|
63
|
+
update: 'paperless:PATCH /api/storage_paths/{id}/',
|
|
64
|
+
},
|
|
65
|
+
fields: [
|
|
66
|
+
{ key: 'path', live: (r) => r['path'], prop: (p) => p.path },
|
|
67
|
+
{ key: 'match', live: (r) => r['match'], prop: (p) => p.match },
|
|
68
|
+
{
|
|
69
|
+
key: 'matching_algorithm',
|
|
70
|
+
live: (r) => r['matching_algorithm'],
|
|
71
|
+
prop: (p) => p.matchingAlgorithm,
|
|
72
|
+
},
|
|
73
|
+
{ key: 'is_insensitive', live: (r) => r['is_insensitive'], prop: (p) => p.isInsensitive },
|
|
74
|
+
],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const StoragePathProvider = () =>
|
|
78
|
+
Provider.effect(StoragePath, Effect.succeed(StoragePath.Provider.of(handlers)));
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Paperless.Tag` — a Paperless-ngx tag, create-or-update and never deleted by default.
|
|
3
|
+
*
|
|
4
|
+
* ★ `matchingAlgorithm` AND THE OTHER ENUM-SHAPED FIELDS ARE INDEXED OFF THE GENERATED REQUEST
|
|
5
|
+
* TYPE (`PaperlessTagRequest['matching_algorithm']`), NEVER RETYPED. The literal union `0 | 1 |
|
|
6
|
+
* … | 6` comes from `codegen/paperless.ts`'s reading of the vendor's own `MatchingAlgorithm`
|
|
7
|
+
* enum — copying it by hand here would be exactly the guess `schema-provenance` doctrine
|
|
8
|
+
* forbids, and a vendor that adds a seventh algorithm would leave this type silently stale.
|
|
9
|
+
*
|
|
10
|
+
* ⛔ NO CREDENTIAL IS A PROP. `PAPERLESS_URL`/`PAPERLESS_TOKEN` come from `credentials.ts`.
|
|
11
|
+
* ⛔ `defaultRemovalPolicy: 'retain'` — the reconciler this replaces never deletes taxonomy, and
|
|
12
|
+
* a tag's id is referenced by every document it is attached to.
|
|
13
|
+
*/
|
|
14
|
+
import { Resource } from 'alchemy';
|
|
15
|
+
import * as Provider from 'alchemy/Provider';
|
|
16
|
+
import * as Effect from 'effect/Effect';
|
|
17
|
+
import type { PaperlessTagRequest } from './generated/types/tags.ts';
|
|
18
|
+
import { type MatchingProps, type MatchingRequirements, matchingHandlers } from './matching.ts';
|
|
19
|
+
|
|
20
|
+
export interface TagProps extends MatchingProps {
|
|
21
|
+
/** ⚠️ maxLength 128 (generated constraint table). The locate key — see matching.ts's header. */
|
|
22
|
+
name: string;
|
|
23
|
+
/** ⚠️ minLength 1, maxLength 7 (a CSS hex color, generated table doesn't enforce the `#`). */
|
|
24
|
+
color?: string;
|
|
25
|
+
/** ⚠️ maxLength 256. The text or pattern documents are matched against. */
|
|
26
|
+
match?: string;
|
|
27
|
+
matchingAlgorithm?: PaperlessTagRequest['matching_algorithm'];
|
|
28
|
+
isInsensitive?: boolean;
|
|
29
|
+
/** Marks this tag as an inbox tag: newly consumed documents are tagged with it. */
|
|
30
|
+
isInboxTag?: boolean;
|
|
31
|
+
/** Another `Paperless.Tag`'s id — Paperless allows one level of tag nesting. */
|
|
32
|
+
parent?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TagAttributes {
|
|
36
|
+
id: number;
|
|
37
|
+
name: string;
|
|
38
|
+
color: string;
|
|
39
|
+
match: string;
|
|
40
|
+
matchingAlgorithm: number;
|
|
41
|
+
isInsensitive: boolean;
|
|
42
|
+
isInboxTag: boolean;
|
|
43
|
+
owner: number | undefined;
|
|
44
|
+
parent: number | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface Tag extends Resource<
|
|
48
|
+
'Paperless.Tag',
|
|
49
|
+
TagProps,
|
|
50
|
+
TagAttributes,
|
|
51
|
+
never,
|
|
52
|
+
MatchingRequirements
|
|
53
|
+
> {}
|
|
54
|
+
|
|
55
|
+
export const Tag = Resource<Tag>('Paperless.Tag', { defaultRemovalPolicy: 'retain' });
|
|
56
|
+
|
|
57
|
+
const handlers = matchingHandlers<TagProps, TagAttributes>({
|
|
58
|
+
attributes: (live, props) => {
|
|
59
|
+
const id = live['id'];
|
|
60
|
+
if (typeof id !== 'number') return undefined;
|
|
61
|
+
return {
|
|
62
|
+
color: typeof live['color'] === 'string' ? live['color'] : '',
|
|
63
|
+
id,
|
|
64
|
+
isInboxTag: live['is_inbox_tag'] === true,
|
|
65
|
+
isInsensitive: live['is_insensitive'] === true,
|
|
66
|
+
match: typeof live['match'] === 'string' ? live['match'] : '',
|
|
67
|
+
matchingAlgorithm:
|
|
68
|
+
typeof live['matching_algorithm'] === 'number' ? live['matching_algorithm'] : 0,
|
|
69
|
+
name: typeof live['name'] === 'string' ? live['name'] : props.name,
|
|
70
|
+
owner: typeof live['owner'] === 'number' ? live['owner'] : undefined,
|
|
71
|
+
parent: typeof live['parent'] === 'number' ? live['parent'] : undefined,
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
collection: 'tags',
|
|
75
|
+
describe: (props) => `tags ${props.name}`,
|
|
76
|
+
endpoint: { create: 'paperless:POST /api/tags/', update: 'paperless:PATCH /api/tags/{id}/' },
|
|
77
|
+
fields: [
|
|
78
|
+
{ key: 'color', live: (r) => r['color'], prop: (p) => p.color },
|
|
79
|
+
{ key: 'match', live: (r) => r['match'], prop: (p) => p.match },
|
|
80
|
+
{
|
|
81
|
+
key: 'matching_algorithm',
|
|
82
|
+
live: (r) => r['matching_algorithm'],
|
|
83
|
+
prop: (p) => p.matchingAlgorithm,
|
|
84
|
+
},
|
|
85
|
+
{ key: 'is_insensitive', live: (r) => r['is_insensitive'], prop: (p) => p.isInsensitive },
|
|
86
|
+
{ key: 'is_inbox_tag', live: (r) => r['is_inbox_tag'], prop: (p) => p.isInboxTag },
|
|
87
|
+
{ key: 'parent', live: (r) => r['parent'], prop: (p) => p.parent },
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export const TagProvider = () => Provider.effect(Tag, Effect.succeed(Tag.Provider.of(handlers)));
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a database's cluster comes from, and how each lifecycle operation gets a client.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ NO ESTATE VALUE LIVES HERE. The kit is PUBLIC; the mini's socket path, its user and its
|
|
5
|
+
* database names are the CONSUMER's props, passed to `postgresConnection(...)` in a house
|
|
6
|
+
* stack — never a default, never a fallback (`docs/postgres.md#path`).
|
|
7
|
+
* ★ A LAZY `Context.Service` WHOSE VALUE IS AN `Effect` (S24), THE SAME SHAPE THE HOUSE USES FOR
|
|
8
|
+
* CREDENTIALS: `yield* yield* PostgresConnection` resolves the config INSIDE each operation,
|
|
9
|
+
* never at layer build. Nothing here holds a secret today (`PostgresConnectionConfig` has no
|
|
10
|
+
* `password` — S25, and CREATE DATABASE has none to assert either), but the shape is the one a
|
|
11
|
+
* later TLS path (`Config.redacted` from the environment) drops into without a rewrite.
|
|
12
|
+
* ★ THE POOL IS BUILT IN A SCOPE AND CLOSED PER OPERATION. `Effect.provide(op, PgClient.layer(config))`
|
|
13
|
+
* already opens a scope for the layer's build and closes it when `op` finishes — one pool per
|
|
14
|
+
* `withPg` call, never held across a whole `reconcile`.
|
|
15
|
+
*/
|
|
16
|
+
import * as PgClient from '@effect/sql-pg/PgClient';
|
|
17
|
+
import * as Context from 'effect/Context';
|
|
18
|
+
import * as Effect from 'effect/Effect';
|
|
19
|
+
import * as Layer from 'effect/Layer';
|
|
20
|
+
import type { SqlError } from 'effect/unstable/sql/SqlError';
|
|
21
|
+
|
|
22
|
+
/** What a stack passes to reach one cluster. `host` may be a socket directory (one beginning
|
|
23
|
+
* with `/` expands to `${host}/.s.PGSQL.${port}` — measured in
|
|
24
|
+
* `node_modules/@effect/sql-pg/src/PgConnection.ts`, the "Details" doc on `Config`) or a TCP
|
|
25
|
+
* host name; `port` defaults to `@effect/sql-pg`'s own default (5432) when omitted. */
|
|
26
|
+
export interface PostgresConnectionConfig {
|
|
27
|
+
readonly host: string;
|
|
28
|
+
readonly port?: number;
|
|
29
|
+
readonly database: string;
|
|
30
|
+
readonly username: string;
|
|
31
|
+
readonly ssl?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The lazy connection service. Its value is an `Effect` of the config, per S24 — see the file
|
|
35
|
+
* header for why. */
|
|
36
|
+
export class PostgresConnection extends Context.Service<
|
|
37
|
+
PostgresConnection,
|
|
38
|
+
Effect.Effect<PostgresConnectionConfig>
|
|
39
|
+
>()('Postgres.Connection') {}
|
|
40
|
+
|
|
41
|
+
/** A stack's one-line way to provide a cluster: `Layer.provide(postgresConnection({ … }))` on
|
|
42
|
+
* `PostgresDatabaseProvider()`, or merged into the stack's own layer tree. */
|
|
43
|
+
export const postgresConnection = (
|
|
44
|
+
config: PostgresConnectionConfig,
|
|
45
|
+
): Layer.Layer<PostgresConnection> => Layer.succeed(PostgresConnection, Effect.succeed(config));
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run one operation against a freshly built, freshly closed connection pool.
|
|
49
|
+
*
|
|
50
|
+
* Every `read`, `reconcile` step and `delete` in this family calls this once for the whole
|
|
51
|
+
* operation — never once per statement — so a reconcile that runs a `SELECT`, then a `CREATE`,
|
|
52
|
+
* then a re-read shares one pool and closes it once, while a plan-only `read` still opens and
|
|
53
|
+
* closes its own.
|
|
54
|
+
*/
|
|
55
|
+
export const withPg = <A, E>(
|
|
56
|
+
build: (pg: PgClient.PgClient) => Effect.Effect<A, E>,
|
|
57
|
+
): Effect.Effect<A, E | SqlError, PostgresConnection> =>
|
|
58
|
+
Effect.gen(function* () {
|
|
59
|
+
const resolveConfig = yield* PostgresConnection;
|
|
60
|
+
const config = yield* resolveConfig;
|
|
61
|
+
return yield* Effect.provide(Effect.flatMap(PgClient.PgClient, build), PgClient.layer(config));
|
|
62
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Postgres.Database` props and attributes, and the one prop-level check that runs before any
|
|
3
|
+
* statement: the name's byte length against `NAMEDATALEN`.
|
|
4
|
+
*
|
|
5
|
+
* ⛔ EVERY ASSERTED PROP HERE HAS A CITED COLUMN. `docs/postgres.md#mapping` and
|
|
6
|
+
* `database-provenance.test.ts` are what make that true rather than asserted: the test parses
|
|
7
|
+
* the two committed fixtures (`create_database.sgml`'s synopsis, `pg_database.h`'s struct) and
|
|
8
|
+
* fails if a prop here has no synopsis option or no catalog column behind it.
|
|
9
|
+
* ⛔ NO PASSWORD, EVER. A database has none in `pg_database` (S25) — `CREATE DATABASE` has no
|
|
10
|
+
* password option at all (`gram.y@REL_18_6#createdb_opt_name`, measured: `IDENT | CONNECTION
|
|
11
|
+
* LIMIT | ENCODING | LOCATION | OWNER | TABLESPACE | TEMPLATE`).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** NAMEDATALEN (`pg_config_manual.h@REL_18_6` line 29) minus the C string terminator: the
|
|
15
|
+
* longest name the server stores without truncating. */
|
|
16
|
+
export const POSTGRES_NAME_MAX_BYTES = 63;
|
|
17
|
+
|
|
18
|
+
/** UTF-8 byte length, the same unit `NAMEDATALEN` counts in (`scansup.c@REL_18_6#truncate_identifier`
|
|
19
|
+
* clips by `pg_mbcliplen`, a byte-oriented, multibyte-aware clip — never a JS `.length`, which
|
|
20
|
+
* counts UTF-16 code units and would pass a 63-byte multibyte name that is really 70+ bytes, or
|
|
21
|
+
* refuse a 63-byte name whose `.length` looks smaller). */
|
|
22
|
+
export const utf8ByteLength = (value: string): number => new TextEncoder().encode(value).byteLength;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Declared, at-most-once-asserted properties of one database.
|
|
26
|
+
*
|
|
27
|
+
* Every prop here maps to a `CREATE DATABASE` `WITH` option `create_database.sgml` documents
|
|
28
|
+
* AND a `pg_database` column that stores it (`database-provenance.test.ts`). `TEMPLATE`,
|
|
29
|
+
* `STRATEGY` and `OID` are excluded because nothing in `pg_database` reads them back once the
|
|
30
|
+
* database exists; `ICU_RULES` and `COLLATION_VERSION` are excluded for having no consumer in
|
|
31
|
+
* this family today. See `docs/postgres.md#excluded`.
|
|
32
|
+
*/
|
|
33
|
+
export interface PostgresDatabaseProps {
|
|
34
|
+
/** At most 63 UTF-8 bytes — refused at plan past that (`PostgresDatabaseNameRefused`). The
|
|
35
|
+
* identity: a name change is a plan-time refusal, never a replace. */
|
|
36
|
+
readonly name: string;
|
|
37
|
+
/** A role name in `pg_roles`, asserted before create (`PostgresDatabaseOwnerMissing`) and
|
|
38
|
+
* compared against `pg_get_userbyid(datdba)` on every plan thereafter. */
|
|
39
|
+
readonly owner: string;
|
|
40
|
+
/** `pg_encoding_to_char(datdatabase.encoding)`, e.g. `"UTF8"`. @default the template's. */
|
|
41
|
+
readonly encoding?: string;
|
|
42
|
+
/** `'builtin' | 'icu' | 'libc'` — `pg_database.datlocprovider` decoded
|
|
43
|
+
* (`pg_collation.h@REL_18_6#collprovider_name`). @default the template's. */
|
|
44
|
+
readonly localeProvider?: 'builtin' | 'icu' | 'libc';
|
|
45
|
+
/** `pg_database.datcollate`. @default the template's. */
|
|
46
|
+
readonly lcCollate?: string;
|
|
47
|
+
/** `pg_database.datctype`. @default the template's. */
|
|
48
|
+
readonly lcCtype?: string;
|
|
49
|
+
/** `pg_database.datallowconn`. @default `true`. */
|
|
50
|
+
readonly allowConnections?: boolean;
|
|
51
|
+
/** `pg_database.datconnlimit`; `-1` means unlimited. @default `-1`. */
|
|
52
|
+
readonly connectionLimit?: number;
|
|
53
|
+
/** `pg_database.datistemplate`. @default `false`. */
|
|
54
|
+
readonly isTemplate?: boolean;
|
|
55
|
+
/** A tablespace name in `pg_tablespace`. @default the template's (usually `pg_default`). */
|
|
56
|
+
readonly tablespace?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Live attributes, read back after every reconcile. No `password` — see the file header. */
|
|
60
|
+
export interface PostgresDatabaseAttributes {
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly oid: number;
|
|
63
|
+
readonly owner: string;
|
|
64
|
+
readonly encoding: string;
|
|
65
|
+
readonly localeProvider: 'builtin' | 'icu' | 'libc';
|
|
66
|
+
readonly collate: string;
|
|
67
|
+
readonly ctype: string;
|
|
68
|
+
readonly allowConnections: boolean;
|
|
69
|
+
readonly connectionLimit: number;
|
|
70
|
+
readonly isTemplate: boolean;
|
|
71
|
+
readonly tablespace: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Refuse a name the server would truncate, before any statement is built.
|
|
76
|
+
*
|
|
77
|
+
* ★ CALLED FROM `diff` (so a plan fails, never a deploy) AND FROM `reconcile` (so a fake or a
|
|
78
|
+
* direct call gets the same refusal, never just a broken statement).
|
|
79
|
+
*/
|
|
80
|
+
export const nameByteRefusal = (
|
|
81
|
+
name: string,
|
|
82
|
+
): { readonly byteLength: number; readonly limit: number } | undefined => {
|
|
83
|
+
const byteLength = utf8ByteLength(name);
|
|
84
|
+
return byteLength > POSTGRES_NAME_MAX_BYTES
|
|
85
|
+
? { byteLength, limit: POSTGRES_NAME_MAX_BYTES }
|
|
86
|
+
: undefined;
|
|
87
|
+
};
|