@murumets-ee/yhikas-sync 0.92.0 → 0.94.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 +108 -0
- package/dist/client-J_w85GUr.mjs +2 -0
- package/dist/client-J_w85GUr.mjs.map +1 -0
- package/dist/config-DM3zAAhm.d.mts +244 -0
- package/dist/config-DM3zAAhm.d.mts.map +1 -0
- package/dist/constants-Bmm3P17G.mjs +2 -0
- package/dist/constants-Bmm3P17G.mjs.map +1 -0
- package/dist/en-BOWwWf-X.mjs +2 -0
- package/dist/en-BOWwWf-X.mjs.map +1 -0
- package/dist/et-CGDUOj83.mjs +2 -0
- package/dist/et-CGDUOj83.mjs.map +1 -0
- package/dist/i18n.d.mts +18 -0
- package/dist/i18n.d.mts.map +1 -0
- package/dist/i18n.mjs +2 -0
- package/dist/i18n.mjs.map +1 -0
- package/dist/index.d.mts +360 -900
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -0
- package/dist/jobs-Di4Z8go9.mjs +2 -0
- package/dist/jobs-Di4Z8go9.mjs.map +1 -0
- package/dist/plugin.d.mts +6 -4
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/rolldown-runtime-DK3Fl9T5.mjs +1 -0
- package/dist/room-page-registry-BFbyGTC7.mjs +2 -0
- package/dist/room-page-registry-BFbyGTC7.mjs.map +1 -0
- package/dist/room-page-registry-CnqXH_Ls.d.mts +974 -0
- package/dist/room-page-registry-CnqXH_Ls.d.mts.map +1 -0
- package/dist/room-pages-adapters-DxFWkFFF.mjs +2 -0
- package/dist/room-pages-adapters-DxFWkFFF.mjs.map +1 -0
- package/dist/room-pages-table-Vd9BqTSl.mjs +2 -0
- package/dist/room-pages-table-Vd9BqTSl.mjs.map +1 -0
- package/dist/room-type-DtMaQbza.mjs +2 -0
- package/dist/room-type-DtMaQbza.mjs.map +1 -0
- package/dist/ru-uzvamDhj.mjs +2 -0
- package/dist/ru-uzvamDhj.mjs.map +1 -0
- package/dist/sync-state-table-CZjRypK0.mjs +2 -0
- package/dist/sync-state-table-CZjRypK0.mjs.map +1 -0
- package/dist/widgets-client.d.mts +27 -0
- package/dist/widgets-client.d.mts.map +1 -0
- package/dist/widgets-client.mjs +3 -0
- package/dist/widgets-client.mjs.map +1 -0
- package/dist/widgets.d.mts +73 -0
- package/dist/widgets.d.mts.map +1 -0
- package/dist/widgets.mjs +2 -0
- package/dist/widgets.mjs.map +1 -0
- package/package.json +41 -7
- package/dist/config-CHVLiXZf.d.mts +0 -77
- package/dist/config-CHVLiXZf.d.mts.map +0 -1
- package/dist/jobs-QeVJDpW9.mjs +0 -2
- package/dist/jobs-QeVJDpW9.mjs.map +0 -1
- package/dist/sync-state-table-BrcoCu-Z.mjs +0 -2
- package/dist/sync-state-table-BrcoCu-Z.mjs.map +0 -1
- package/dist/watchdog-CcOXpzE3.mjs +0 -2
- package/dist/watchdog-CcOXpzE3.mjs.map +0 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,768 +1,9 @@
|
|
|
1
|
-
import { i as
|
|
1
|
+
import { $ as EN_LOCALE, A as secondaryLocaleOf, B as roomTypesResponseSchema, C as ProjectedRow, D as projectLegalDocuments, E as SkippedRow, F as SiteNoticeRow, G as PlanDiffInput, H as siteNoticeRowSchema, I as legalDocumentRowSchema, J as stableHash, K as SanityFloor, L as legalDocumentsResponseSchema, M as MultilingualText, N as RoomTypeRow, O as projectRoomTypes, P as SiteInfoResponse, Q as DECLARED_VAT_TREATMENT, R as multilingualTextSchema, S as HtmlSanitizer, T as ProjectionResult, U as DiffPlan, V as siteInfoResponseSchema, W as LocalRow, X as BASE_URL_ENV_VAR, Y as API_KEY_ENV_VAR, Z as DECLARED_CURRENCY, _ as SyncEntityClient, a as YhikasRoomPageRecordError, at as SITE_INFO_RESOURCE, b as applyPlan, c as ROOM_PAGE_ORIGINS, ct as SyncResource, d as RoomPageRefusalKind, dt as YHIKAS_SYNC_PLUGIN_NAME, et as ET_LOCALE, f as yhikasRoomPagesTable, g as ApplyPlanInput, h as ApplyCounts, i as RoomPageRegistry, it as SITE_INFO_KEY, j as LegalDocumentRow, k as projectSiteInfo, l as ROOM_PAGE_REFUSAL_KINDS, lt as WATCHDOG_JOB_NAME, m as toSyncEntityClient, n as RoomPageRecord, nt as MAX_SITE_NOTICES, o as createRoomPageRegistry, ot as SYNC_JOB_NAME, p as AdminClientLike, q as planDiff, r as RoomPageRefusal, rt as ROOM_TYPES_RESOURCE, s as ROOM_PAGE_CLAIM_LEASE_MS, st as SYNC_RESOURCES, t as ROOM_PAGE_READ_CHUNK, tt as LEGAL_DOCUMENTS_RESOURCE, u as RoomPageOrigin, ut as YHIKAS_SYNC_ACTOR_ID, v as SyncLogger, w as ProjectionOptions, x as readLocalRows, y as YhikasApplyPartialError, z as roomTypeRowSchema } from "./room-page-registry-CnqXH_Ls.mjs";
|
|
2
|
+
import { a as RoomPageDraft, c as YhikasSyncConfigError, d as isPublishedRoomType, f as roomTypeForPageFrom, g as isRoomOffer, h as RoomOfferPredicate, i as RoomPageBlock, l as resolveYhikasSyncConfig, m as RoomOfferCandidate, n as ResolvedRoomPagesConfig, o as YhikasRoomPagesConfig, p as AREA_PRICED_UNIT_NAMES, r as ResolvedYhikasSyncConfig, s as YhikasSyncConfig, t as PAGE_SLUG_PATTERN, u as RoomTypeForPage } from "./config-DM3zAAhm.mjs";
|
|
2
3
|
import { Behavior } from "@murumets-ee/entity";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { PgColumn } from "drizzle-orm/pg-core";
|
|
5
|
+
import { LiveFactSpec, LiveFreshness, LiveItem, LiveSourceSpec } from "@murumets-ee/blocks/schema";
|
|
5
6
|
|
|
6
|
-
//#region src/constants.d.ts
|
|
7
|
-
/**
|
|
8
|
-
* Names, identifiers and stated numbers for the yhikas-admin sync.
|
|
9
|
-
*
|
|
10
|
-
* Pure module — no imports, no side effects. Both the jiti-loaded plugin entry
|
|
11
|
-
* and the worker-side job modules read from here, so nothing in this file may
|
|
12
|
-
* reach a `server-only` module.
|
|
13
|
-
*/
|
|
14
|
-
/** Plugin name, as it appears in `app.plugins` and `Plugin.requires`. */
|
|
15
|
-
declare const YHIKAS_SYNC_PLUGIN_NAME = "@murumets-ee/yhikas-sync";
|
|
16
|
-
/**
|
|
17
|
-
* The synthetic actor every sync write is attributed to.
|
|
18
|
-
*
|
|
19
|
-
* Not `'cli'`. `auditable()` records this in `created_by`/`updated_by`, so a
|
|
20
|
-
* human reading the audit log can tell a sync write from an operator running a
|
|
21
|
-
* `lumi` command — and the machine-written guard (see `entities/guard.ts`) can
|
|
22
|
-
* refuse every other writer by name. Fits `varchar(255)`; user ids in this
|
|
23
|
-
* codebase are varchar, never uuid.
|
|
24
|
-
*/
|
|
25
|
-
declare const YHIKAS_SYNC_ACTOR_ID = "yhikas-sync";
|
|
26
|
-
/**
|
|
27
|
-
* Job names. `JOB_NAME_RE` in `@murumets-ee/queue` is
|
|
28
|
-
* `/^[a-zA-Z0-9:_-]{1,128}$/` — colons are the `plugin:action` separator and
|
|
29
|
-
* dots are rejected outright.
|
|
30
|
-
*/
|
|
31
|
-
declare const SYNC_JOB_NAME = "yhikas-sync:pull";
|
|
32
|
-
declare const WATCHDOG_JOB_NAME = "yhikas-sync:staleness-watchdog";
|
|
33
|
-
/**
|
|
34
|
-
* The three resources in scope.
|
|
35
|
-
*
|
|
36
|
-
* `site_info` was deliberately absent until 2026-08-11 (D027). PR 05 left it
|
|
37
|
-
* out on a MEASUREMENT — `site_info`/`site_notice` appear only in
|
|
38
|
-
* yhikas-admin's `src/db/schema.ts` and its public route, with no admin UI, no
|
|
39
|
-
* server action, no seed and no internal consumer — from which the plan
|
|
40
|
-
* concluded that no rows existed and a sync would pull nothing.
|
|
41
|
-
*
|
|
42
|
-
* Measured against production on 2026-08-11 (R028), that premise had expired:
|
|
43
|
-
* the route answers 200 with populated reception hours and two live notices.
|
|
44
|
-
* The rows were written outside the app. Hardcoding them in the rebuilt site
|
|
45
|
-
* would therefore have frozen live data, not stood in for an empty upstream.
|
|
46
|
-
*/
|
|
47
|
-
declare const ROOM_TYPES_RESOURCE = "room_types";
|
|
48
|
-
declare const LEGAL_DOCUMENTS_RESOURCE = "legal_documents";
|
|
49
|
-
declare const SITE_INFO_RESOURCE = "site_info";
|
|
50
|
-
declare const SYNC_RESOURCES: readonly ["room_types", "legal_documents", "site_info"];
|
|
51
|
-
type SyncResource = (typeof SYNC_RESOURCES)[number];
|
|
52
|
-
/**
|
|
53
|
-
* The business key of the one `yhikas_site_info` row.
|
|
54
|
-
*
|
|
55
|
-
* Upstream `site_info` is a singleton and its notices carry no id or code of
|
|
56
|
-
* their own — only `{text, isActive, order}` — so there is no natural key to
|
|
57
|
-
* diff N rows on. Modelling it as ONE row with a constant key keeps the whole
|
|
58
|
-
* package on a single identity story (`code`, `type`, and now `key`), and
|
|
59
|
-
* keeps the notices ordered without inventing an identity that upstream would
|
|
60
|
-
* not preserve across an edit.
|
|
61
|
-
*/
|
|
62
|
-
declare const SITE_INFO_KEY = "default";
|
|
63
|
-
/**
|
|
64
|
-
* Locale codes. Upstream `MultilingualText` is `{ en, et }` and the lumi codes
|
|
65
|
-
* are identical, so there is no mapping to configure — inventing one would be
|
|
66
|
-
* a seam for a second customer this package does not have (N1).
|
|
67
|
-
*/
|
|
68
|
-
declare const ET_LOCALE = "et";
|
|
69
|
-
declare const EN_LOCALE = "en";
|
|
70
|
-
/**
|
|
71
|
-
* Currency and VAT treatment, DECLARED here because they are not data upstream
|
|
72
|
-
* (R009 §6 / R013): `room_type` has no currency column and no tax column, and
|
|
73
|
-
* VAT is computed at invoice time against Merit article codes, never stored.
|
|
74
|
-
* Stating them makes a mis-entered source value render as a visibly wrong
|
|
75
|
-
* number against a declared unit rather than as a plausible one.
|
|
76
|
-
*
|
|
77
|
-
* The third semantic — the period — stays encoded in the field NAME
|
|
78
|
-
* (`monthlyRent` / `dailyRent`), because one row carries two different periods
|
|
79
|
-
* and a single `period` column could only be wrong about one of them.
|
|
80
|
-
*/
|
|
81
|
-
declare const DECLARED_CURRENCY = "EUR";
|
|
82
|
-
/** `net` = the amount excludes VAT. */
|
|
83
|
-
declare const DECLARED_VAT_TREATMENT = "net";
|
|
84
|
-
/**
|
|
85
|
-
* Hard ceiling on ticker items. A ticker is a marquee: past a couple of dozen
|
|
86
|
-
* entries nobody reads the tail, and an unbounded array from upstream would
|
|
87
|
-
* ride into a JSONB column and out onto every page of the site.
|
|
88
|
-
*/
|
|
89
|
-
declare const MAX_SITE_NOTICES = 25;
|
|
90
|
-
/**
|
|
91
|
-
* Env var carrying the sync's OWN bearer credential — deliberately not the
|
|
92
|
-
* site's `PUBLIC_SITE_API_KEY`. The upstream limiter buckets on
|
|
93
|
-
* `sha256(key).slice(0,16)` rather than on the caller IP (there is no
|
|
94
|
-
* `x-forwarded-for` over the Docker-internal path), so sharing a key means
|
|
95
|
-
* sharing one 60/min bucket, and revoking one credential would blind both.
|
|
96
|
-
*/
|
|
97
|
-
declare const API_KEY_ENV_VAR = "YHIKAS_SYNC_API_KEY";
|
|
98
|
-
declare const BASE_URL_ENV_VAR = "YHIKAS_ADMIN_BASE_URL";
|
|
99
|
-
//#endregion
|
|
100
|
-
//#region src/diff.d.ts
|
|
101
|
-
/** One local row, reduced to what the diff needs. */
|
|
102
|
-
interface LocalRow {
|
|
103
|
-
readonly id: string;
|
|
104
|
-
/** The business key this row was synced under. */
|
|
105
|
-
readonly key: string;
|
|
106
|
-
readonly status: string;
|
|
107
|
-
/**
|
|
108
|
-
* `null` on a row whose last sync did not complete — the hash is written
|
|
109
|
-
* LAST, after every other write for the row succeeded, so a null (or stale)
|
|
110
|
-
* hash is exactly the signal that the row needs redoing.
|
|
111
|
-
*/
|
|
112
|
-
readonly sourceHash: string | null;
|
|
113
|
-
/** Preserved across updates so it keeps meaning "first published". */
|
|
114
|
-
readonly publishedAt: Date | null;
|
|
115
|
-
}
|
|
116
|
-
interface DiffPlan<T> {
|
|
117
|
-
/** Upstream rows with no local counterpart. */
|
|
118
|
-
readonly create: readonly T[];
|
|
119
|
-
/** Local rows whose hash differs, OR whose status drifted from `published`. */
|
|
120
|
-
readonly update: readonly {
|
|
121
|
-
readonly local: LocalRow;
|
|
122
|
-
readonly row: T;
|
|
123
|
-
}[];
|
|
124
|
-
/** Local rows already identical and already published — no write at all. */
|
|
125
|
-
readonly unchanged: readonly LocalRow[];
|
|
126
|
-
/** Published locally, absent upstream. Unpublished, never deleted (D016). */
|
|
127
|
-
readonly retire: readonly LocalRow[];
|
|
128
|
-
}
|
|
129
|
-
interface SanityFloor {
|
|
130
|
-
/** Refuse a snapshot larger than this rather than truncating it. */
|
|
131
|
-
readonly maxRows: number;
|
|
132
|
-
/** Refuse a run retiring more than this fraction of published rows. */
|
|
133
|
-
readonly maxRetireFraction: number;
|
|
134
|
-
/** The fraction rule applies only once at least this many rows are published. */
|
|
135
|
-
readonly minRowsForFraction: number;
|
|
136
|
-
}
|
|
137
|
-
interface PlanDiffInput<T> {
|
|
138
|
-
readonly resource: SyncResource;
|
|
139
|
-
readonly upstream: readonly T[];
|
|
140
|
-
readonly local: readonly LocalRow[];
|
|
141
|
-
readonly keyOf: (row: T) => string;
|
|
142
|
-
readonly hashOf: (row: T) => string;
|
|
143
|
-
readonly floor: SanityFloor;
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Stable content hash of an upstream row.
|
|
147
|
-
*
|
|
148
|
-
* Keys are sorted so the hash does not depend on JSON property order, which no
|
|
149
|
-
* part of the HTTP stack guarantees. `undefined` and `null` are distinguished
|
|
150
|
-
* because a null price is meaningful data here, not an absence.
|
|
151
|
-
*/
|
|
152
|
-
declare function stableHash(value: unknown): string;
|
|
153
|
-
/**
|
|
154
|
-
* Compare an authoritative upstream snapshot against local state.
|
|
155
|
-
*
|
|
156
|
-
* **The caller must not invoke this with a snapshot that did not fully
|
|
157
|
-
* succeed.** That guard lives one level up, in the client: a non-2xx, a
|
|
158
|
-
* timeout or a body failing the wire schema throws before the differ is ever
|
|
159
|
-
* reached, so a partial response can never present as an absence here. This
|
|
160
|
-
* function's own guards are for a snapshot that IS authoritative but whose
|
|
161
|
-
* shape makes acting on it reckless.
|
|
162
|
-
*
|
|
163
|
-
* @throws {YhikasSyncRefusedError} for any of the D016 refusals. Every one of
|
|
164
|
-
* them aborts before a single write, so a refused run leaves local content
|
|
165
|
-
* exactly as it was — which is the first of PR 05's three obligatory
|
|
166
|
-
* negative tests.
|
|
167
|
-
*/
|
|
168
|
-
declare function planDiff<T>(input: PlanDiffInput<T>): DiffPlan<T>;
|
|
169
|
-
//#endregion
|
|
170
|
-
//#region src/upstream/wire.d.ts
|
|
171
|
-
/**
|
|
172
|
-
* `MultilingualText` — a Postgres `json` column, so it arrives as a nested
|
|
173
|
-
* object and is never stringified.
|
|
174
|
-
*
|
|
175
|
-
* NOT `.strict()`: unknown keys are stripped rather than rejected, so adding a
|
|
176
|
-
* third language upstream degrades to "the sync ignores it" instead of "every
|
|
177
|
-
* run fails".
|
|
178
|
-
*
|
|
179
|
-
* 🔴 **Each locale key is nullable AND optional, and that is the load-bearing
|
|
180
|
-
* clause** (F058, cross-referenced to upstream F031). The column is declared
|
|
181
|
-
* `json(...).notNull()` — and **notNull covers the OBJECT, not its keys**. There
|
|
182
|
-
* is no CHECK constraint, and two of the four public columns
|
|
183
|
-
* (`room_type.name`, `legal_document.title`) have no upstream validation at all:
|
|
184
|
-
* the write is a raw `formData.get(…) as string`, which is `null` the moment a
|
|
185
|
-
* field is absent. So `{"et":"Tuba","en":null}` — and `{"et":"Tuba"}` — are rows
|
|
186
|
-
* yhikas-admin accepts today, through its ordinary admin UI.
|
|
187
|
-
*
|
|
188
|
-
* Requiring both keys would fail the ENTIRE response for one such row, so 32
|
|
189
|
-
* room types would stop syncing because one lacks an English name — every six
|
|
190
|
-
* hours, until somebody edited it upstream. That is exactly the blast-radius
|
|
191
|
-
* mistake `legalDocumentRowSchema.slug` documents below and deliberately
|
|
192
|
-
* avoided; a missing translation belongs at one row, and `projection.ts` is
|
|
193
|
-
* where it is decided (the locale is suppressed, or the row is skipped with a
|
|
194
|
-
* reason).
|
|
195
|
-
*
|
|
196
|
-
* What is NOT relaxed, on purpose:
|
|
197
|
-
*
|
|
198
|
-
* - **The object itself stays required.** A missing translation is DATA; a
|
|
199
|
-
* missing FIELD is a renamed column or a changed envelope. The consequence of
|
|
200
|
-
* tolerating one is caught either way — `assertSomethingSurvived` refuses a
|
|
201
|
-
* snapshot in which no row survived — but refusing here NAMES it
|
|
202
|
-
* (`title: Required`) instead of reporting 32 identical "no 'et' title
|
|
203
|
-
* upstream" skips and leaving the reader to infer the cause.
|
|
204
|
-
* - **A non-string value is still a shape violation.** `{"en":42}` would
|
|
205
|
-
* otherwise be written as a name.
|
|
206
|
-
*
|
|
207
|
-
* Note the asymmetry this leaves, deliberately: a RENAMED key (`et_EE`) is
|
|
208
|
-
* indistinguishable from two absent ones, because unknown keys are stripped by
|
|
209
|
-
* design so a third language degrades gracefully. That case falls through to
|
|
210
|
-
* `assertSomethingSurvived`, which is exactly what it is for.
|
|
211
|
-
*/
|
|
212
|
-
declare const multilingualTextSchema: z.ZodObject<{
|
|
213
|
-
et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
214
|
-
en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
215
|
-
}, "strip", z.ZodTypeAny, {
|
|
216
|
-
et?: string | null | undefined;
|
|
217
|
-
en?: string | null | undefined;
|
|
218
|
-
}, {
|
|
219
|
-
et?: string | null | undefined;
|
|
220
|
-
en?: string | null | undefined;
|
|
221
|
-
}>;
|
|
222
|
-
type MultilingualText = z.infer<typeof multilingualTextSchema>;
|
|
223
|
-
/**
|
|
224
|
-
* One `room_type` row.
|
|
225
|
-
*
|
|
226
|
-
* `depositAmount` / `discountedDepositAmount` are absent by design. Upstream
|
|
227
|
-
* ships them as hardcoded `null` with no backing column; validating them as
|
|
228
|
-
* `z.null()` would turn the day someone adds the column into a hard sync
|
|
229
|
-
* failure. Deposits are unbuilt admin-side work, not something the sync can
|
|
230
|
-
* surface.
|
|
231
|
-
*/
|
|
232
|
-
declare const roomTypeRowSchema: z.ZodObject<{
|
|
233
|
-
code: z.ZodString;
|
|
234
|
-
name: z.ZodObject<{
|
|
235
|
-
et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
236
|
-
en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
237
|
-
}, "strip", z.ZodTypeAny, {
|
|
238
|
-
et?: string | null | undefined;
|
|
239
|
-
en?: string | null | undefined;
|
|
240
|
-
}, {
|
|
241
|
-
et?: string | null | undefined;
|
|
242
|
-
en?: string | null | undefined;
|
|
243
|
-
}>;
|
|
244
|
-
totalArea: z.ZodNullable<z.ZodString>;
|
|
245
|
-
livingArea: z.ZodNullable<z.ZodString>;
|
|
246
|
-
commonArea: z.ZodNullable<z.ZodString>;
|
|
247
|
-
capacity: z.ZodNullable<z.ZodNumber>;
|
|
248
|
-
monthlyRent: z.ZodNullable<z.ZodString>;
|
|
249
|
-
discountedRent: z.ZodNullable<z.ZodString>;
|
|
250
|
-
dailyRent: z.ZodNullable<z.ZodString>;
|
|
251
|
-
placesOccupied: z.ZodNullable<z.ZodNumber>;
|
|
252
|
-
}, "strip", z.ZodTypeAny, {
|
|
253
|
-
code: string;
|
|
254
|
-
name: {
|
|
255
|
-
et?: string | null | undefined;
|
|
256
|
-
en?: string | null | undefined;
|
|
257
|
-
};
|
|
258
|
-
totalArea: string | null;
|
|
259
|
-
livingArea: string | null;
|
|
260
|
-
commonArea: string | null;
|
|
261
|
-
capacity: number | null;
|
|
262
|
-
placesOccupied: number | null;
|
|
263
|
-
monthlyRent: string | null;
|
|
264
|
-
dailyRent: string | null;
|
|
265
|
-
discountedRent: string | null;
|
|
266
|
-
}, {
|
|
267
|
-
code: string;
|
|
268
|
-
name: {
|
|
269
|
-
et?: string | null | undefined;
|
|
270
|
-
en?: string | null | undefined;
|
|
271
|
-
};
|
|
272
|
-
totalArea: string | null;
|
|
273
|
-
livingArea: string | null;
|
|
274
|
-
commonArea: string | null;
|
|
275
|
-
capacity: number | null;
|
|
276
|
-
placesOccupied: number | null;
|
|
277
|
-
monthlyRent: string | null;
|
|
278
|
-
dailyRent: string | null;
|
|
279
|
-
discountedRent: string | null;
|
|
280
|
-
}>;
|
|
281
|
-
type RoomTypeRow = z.infer<typeof roomTypeRowSchema>;
|
|
282
|
-
/**
|
|
283
|
-
* One active `legal_document` row.
|
|
284
|
-
*
|
|
285
|
-
* `type` is `text().notNull().unique()` upstream — NOT a pgEnum, and there is
|
|
286
|
-
* no TS union anywhere. The five values seeded today are closed by convention
|
|
287
|
-
* only and the admin UI can mint a sixth, so this validates the SHAPE of the
|
|
288
|
-
* business key and never its membership in a list. A whitelist here would turn
|
|
289
|
-
* a new upstream document into a hard sync failure.
|
|
290
|
-
*/
|
|
291
|
-
declare const legalDocumentRowSchema: z.ZodObject<{
|
|
292
|
-
type: z.ZodString;
|
|
293
|
-
title: z.ZodObject<{
|
|
294
|
-
et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
295
|
-
en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
296
|
-
}, "strip", z.ZodTypeAny, {
|
|
297
|
-
et?: string | null | undefined;
|
|
298
|
-
en?: string | null | undefined;
|
|
299
|
-
}, {
|
|
300
|
-
et?: string | null | undefined;
|
|
301
|
-
en?: string | null | undefined;
|
|
302
|
-
}>;
|
|
303
|
-
/**
|
|
304
|
-
* Accepted as free text HERE, and screened per-row in the projection.
|
|
305
|
-
*
|
|
306
|
-
* Upstream derives it from an unvalidated free-text form field
|
|
307
|
-
* (`type.toLowerCase().replace(/_/g, '-')`), so an Estonian title yields an
|
|
308
|
-
* Estonian slug — `üldtingimused` — and a title with a space yields a slug
|
|
309
|
-
* with a space. A character-class regex on the RESPONSE schema would fail
|
|
310
|
-
* `legalDocumentsResponseSchema` for the whole payload, so one newly
|
|
311
|
-
* authored document would take the entire resource offline every six hours
|
|
312
|
-
* until someone edited it upstream. The blast radius belongs at one row.
|
|
313
|
-
*/
|
|
314
|
-
slug: z.ZodString;
|
|
315
|
-
htmlContentEt: z.ZodString;
|
|
316
|
-
htmlContentEn: z.ZodString;
|
|
317
|
-
order: z.ZodNumber;
|
|
318
|
-
}, "strip", z.ZodTypeAny, {
|
|
319
|
-
type: string;
|
|
320
|
-
slug: string;
|
|
321
|
-
title: {
|
|
322
|
-
et?: string | null | undefined;
|
|
323
|
-
en?: string | null | undefined;
|
|
324
|
-
};
|
|
325
|
-
order: number;
|
|
326
|
-
htmlContentEt: string;
|
|
327
|
-
htmlContentEn: string;
|
|
328
|
-
}, {
|
|
329
|
-
type: string;
|
|
330
|
-
slug: string;
|
|
331
|
-
title: {
|
|
332
|
-
et?: string | null | undefined;
|
|
333
|
-
en?: string | null | undefined;
|
|
334
|
-
};
|
|
335
|
-
order: number;
|
|
336
|
-
htmlContentEt: string;
|
|
337
|
-
htmlContentEn: string;
|
|
338
|
-
}>;
|
|
339
|
-
type LegalDocumentRow = z.infer<typeof legalDocumentRowSchema>;
|
|
340
|
-
/**
|
|
341
|
-
* `success: z.literal(true)` is the load-bearing clause, not decoration.
|
|
342
|
-
*
|
|
343
|
-
* Every upstream failure path sets `success: false` — 401 (missing header,
|
|
344
|
-
* wrong scheme, wrong key, AND an unset server-side key: all four
|
|
345
|
-
* indistinguishable, deny-by-default through one branch), 429, and 500. No
|
|
346
|
-
* route returns 200 with a degraded body. So the discriminator is `success`,
|
|
347
|
-
* never array length, and a snapshot that fails this schema can never be
|
|
348
|
-
* mistaken for an authoritative empty one.
|
|
349
|
-
*/
|
|
350
|
-
declare const roomTypesResponseSchema: z.ZodObject<{
|
|
351
|
-
success: z.ZodLiteral<true>;
|
|
352
|
-
roomTypes: z.ZodArray<z.ZodObject<{
|
|
353
|
-
code: z.ZodString;
|
|
354
|
-
name: z.ZodObject<{
|
|
355
|
-
et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
356
|
-
en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
357
|
-
}, "strip", z.ZodTypeAny, {
|
|
358
|
-
et?: string | null | undefined;
|
|
359
|
-
en?: string | null | undefined;
|
|
360
|
-
}, {
|
|
361
|
-
et?: string | null | undefined;
|
|
362
|
-
en?: string | null | undefined;
|
|
363
|
-
}>;
|
|
364
|
-
totalArea: z.ZodNullable<z.ZodString>;
|
|
365
|
-
livingArea: z.ZodNullable<z.ZodString>;
|
|
366
|
-
commonArea: z.ZodNullable<z.ZodString>;
|
|
367
|
-
capacity: z.ZodNullable<z.ZodNumber>;
|
|
368
|
-
monthlyRent: z.ZodNullable<z.ZodString>;
|
|
369
|
-
discountedRent: z.ZodNullable<z.ZodString>;
|
|
370
|
-
dailyRent: z.ZodNullable<z.ZodString>;
|
|
371
|
-
placesOccupied: z.ZodNullable<z.ZodNumber>;
|
|
372
|
-
}, "strip", z.ZodTypeAny, {
|
|
373
|
-
code: string;
|
|
374
|
-
name: {
|
|
375
|
-
et?: string | null | undefined;
|
|
376
|
-
en?: string | null | undefined;
|
|
377
|
-
};
|
|
378
|
-
totalArea: string | null;
|
|
379
|
-
livingArea: string | null;
|
|
380
|
-
commonArea: string | null;
|
|
381
|
-
capacity: number | null;
|
|
382
|
-
placesOccupied: number | null;
|
|
383
|
-
monthlyRent: string | null;
|
|
384
|
-
dailyRent: string | null;
|
|
385
|
-
discountedRent: string | null;
|
|
386
|
-
}, {
|
|
387
|
-
code: string;
|
|
388
|
-
name: {
|
|
389
|
-
et?: string | null | undefined;
|
|
390
|
-
en?: string | null | undefined;
|
|
391
|
-
};
|
|
392
|
-
totalArea: string | null;
|
|
393
|
-
livingArea: string | null;
|
|
394
|
-
commonArea: string | null;
|
|
395
|
-
capacity: number | null;
|
|
396
|
-
placesOccupied: number | null;
|
|
397
|
-
monthlyRent: string | null;
|
|
398
|
-
dailyRent: string | null;
|
|
399
|
-
discountedRent: string | null;
|
|
400
|
-
}>, "many">;
|
|
401
|
-
}, "strip", z.ZodTypeAny, {
|
|
402
|
-
success: true;
|
|
403
|
-
roomTypes: {
|
|
404
|
-
code: string;
|
|
405
|
-
name: {
|
|
406
|
-
et?: string | null | undefined;
|
|
407
|
-
en?: string | null | undefined;
|
|
408
|
-
};
|
|
409
|
-
totalArea: string | null;
|
|
410
|
-
livingArea: string | null;
|
|
411
|
-
commonArea: string | null;
|
|
412
|
-
capacity: number | null;
|
|
413
|
-
placesOccupied: number | null;
|
|
414
|
-
monthlyRent: string | null;
|
|
415
|
-
dailyRent: string | null;
|
|
416
|
-
discountedRent: string | null;
|
|
417
|
-
}[];
|
|
418
|
-
}, {
|
|
419
|
-
success: true;
|
|
420
|
-
roomTypes: {
|
|
421
|
-
code: string;
|
|
422
|
-
name: {
|
|
423
|
-
et?: string | null | undefined;
|
|
424
|
-
en?: string | null | undefined;
|
|
425
|
-
};
|
|
426
|
-
totalArea: string | null;
|
|
427
|
-
livingArea: string | null;
|
|
428
|
-
commonArea: string | null;
|
|
429
|
-
capacity: number | null;
|
|
430
|
-
placesOccupied: number | null;
|
|
431
|
-
monthlyRent: string | null;
|
|
432
|
-
dailyRent: string | null;
|
|
433
|
-
discountedRent: string | null;
|
|
434
|
-
}[];
|
|
435
|
-
}>;
|
|
436
|
-
declare const legalDocumentsResponseSchema: z.ZodObject<{
|
|
437
|
-
success: z.ZodLiteral<true>;
|
|
438
|
-
documents: z.ZodArray<z.ZodObject<{
|
|
439
|
-
type: z.ZodString;
|
|
440
|
-
title: z.ZodObject<{
|
|
441
|
-
et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
442
|
-
en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
443
|
-
}, "strip", z.ZodTypeAny, {
|
|
444
|
-
et?: string | null | undefined;
|
|
445
|
-
en?: string | null | undefined;
|
|
446
|
-
}, {
|
|
447
|
-
et?: string | null | undefined;
|
|
448
|
-
en?: string | null | undefined;
|
|
449
|
-
}>;
|
|
450
|
-
/**
|
|
451
|
-
* Accepted as free text HERE, and screened per-row in the projection.
|
|
452
|
-
*
|
|
453
|
-
* Upstream derives it from an unvalidated free-text form field
|
|
454
|
-
* (`type.toLowerCase().replace(/_/g, '-')`), so an Estonian title yields an
|
|
455
|
-
* Estonian slug — `üldtingimused` — and a title with a space yields a slug
|
|
456
|
-
* with a space. A character-class regex on the RESPONSE schema would fail
|
|
457
|
-
* `legalDocumentsResponseSchema` for the whole payload, so one newly
|
|
458
|
-
* authored document would take the entire resource offline every six hours
|
|
459
|
-
* until someone edited it upstream. The blast radius belongs at one row.
|
|
460
|
-
*/
|
|
461
|
-
slug: z.ZodString;
|
|
462
|
-
htmlContentEt: z.ZodString;
|
|
463
|
-
htmlContentEn: z.ZodString;
|
|
464
|
-
order: z.ZodNumber;
|
|
465
|
-
}, "strip", z.ZodTypeAny, {
|
|
466
|
-
type: string;
|
|
467
|
-
slug: string;
|
|
468
|
-
title: {
|
|
469
|
-
et?: string | null | undefined;
|
|
470
|
-
en?: string | null | undefined;
|
|
471
|
-
};
|
|
472
|
-
order: number;
|
|
473
|
-
htmlContentEt: string;
|
|
474
|
-
htmlContentEn: string;
|
|
475
|
-
}, {
|
|
476
|
-
type: string;
|
|
477
|
-
slug: string;
|
|
478
|
-
title: {
|
|
479
|
-
et?: string | null | undefined;
|
|
480
|
-
en?: string | null | undefined;
|
|
481
|
-
};
|
|
482
|
-
order: number;
|
|
483
|
-
htmlContentEt: string;
|
|
484
|
-
htmlContentEn: string;
|
|
485
|
-
}>, "many">;
|
|
486
|
-
}, "strip", z.ZodTypeAny, {
|
|
487
|
-
success: true;
|
|
488
|
-
documents: {
|
|
489
|
-
type: string;
|
|
490
|
-
slug: string;
|
|
491
|
-
title: {
|
|
492
|
-
et?: string | null | undefined;
|
|
493
|
-
en?: string | null | undefined;
|
|
494
|
-
};
|
|
495
|
-
order: number;
|
|
496
|
-
htmlContentEt: string;
|
|
497
|
-
htmlContentEn: string;
|
|
498
|
-
}[];
|
|
499
|
-
}, {
|
|
500
|
-
success: true;
|
|
501
|
-
documents: {
|
|
502
|
-
type: string;
|
|
503
|
-
slug: string;
|
|
504
|
-
title: {
|
|
505
|
-
et?: string | null | undefined;
|
|
506
|
-
en?: string | null | undefined;
|
|
507
|
-
};
|
|
508
|
-
order: number;
|
|
509
|
-
htmlContentEt: string;
|
|
510
|
-
htmlContentEn: string;
|
|
511
|
-
}[];
|
|
512
|
-
}>;
|
|
513
|
-
/**
|
|
514
|
-
* One `site_notice` — a ticker item.
|
|
515
|
-
*
|
|
516
|
-
* No id, no code, no timestamp: `{text, isActive, order}` is the whole row as
|
|
517
|
-
* the route serves it. That absence is why `site_info` is modelled as one
|
|
518
|
-
* local row carrying an ordered list rather than as N diffable rows — there is
|
|
519
|
-
* no key an edit upstream would preserve.
|
|
520
|
-
*
|
|
521
|
-
* `isActive` is honoured HERE rather than assumed: the route returns inactive
|
|
522
|
-
* notices too, and a ticker that shows a retired notice is worse than one that
|
|
523
|
-
* shows nothing.
|
|
524
|
-
*/
|
|
525
|
-
declare const siteNoticeRowSchema: z.ZodObject<{
|
|
526
|
-
text: z.ZodObject<{
|
|
527
|
-
et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
528
|
-
en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
529
|
-
}, "strip", z.ZodTypeAny, {
|
|
530
|
-
et?: string | null | undefined;
|
|
531
|
-
en?: string | null | undefined;
|
|
532
|
-
}, {
|
|
533
|
-
et?: string | null | undefined;
|
|
534
|
-
en?: string | null | undefined;
|
|
535
|
-
}>;
|
|
536
|
-
isActive: z.ZodBoolean;
|
|
537
|
-
order: z.ZodNumber;
|
|
538
|
-
}, "strip", z.ZodTypeAny, {
|
|
539
|
-
text: {
|
|
540
|
-
et?: string | null | undefined;
|
|
541
|
-
en?: string | null | undefined;
|
|
542
|
-
};
|
|
543
|
-
order: number;
|
|
544
|
-
isActive: boolean;
|
|
545
|
-
}, {
|
|
546
|
-
text: {
|
|
547
|
-
et?: string | null | undefined;
|
|
548
|
-
en?: string | null | undefined;
|
|
549
|
-
};
|
|
550
|
-
order: number;
|
|
551
|
-
isActive: boolean;
|
|
552
|
-
}>;
|
|
553
|
-
type SiteNoticeRow = z.infer<typeof siteNoticeRowSchema>;
|
|
554
|
-
/**
|
|
555
|
-
* `/api/public/site-info` — reception hours plus the notice ticker.
|
|
556
|
-
*
|
|
557
|
-
* 🔴 **This envelope cannot express "not configured yet".** The route answers
|
|
558
|
-
* `200 { success: true, receptionHours: {et:'',en:''}, notices: [] }` when no
|
|
559
|
-
* row exists, which is byte-identical to an operator having deliberately
|
|
560
|
-
* cleared everything (F015). The other two resources have no such hole —
|
|
561
|
-
* theirs discriminate on `success` and on array length against a local
|
|
562
|
-
* snapshot.
|
|
563
|
-
*
|
|
564
|
-
* The schema is therefore NOT where that is solved, and deliberately so: the
|
|
565
|
-
* shape is valid either way. `projectSiteInfo` refuses an all-empty payload as
|
|
566
|
-
* unusable (D027), which is the only place that has the standing to decide
|
|
567
|
-
* that a well-formed response is not authoritative. Upstream's later
|
|
568
|
-
* `updatedAt` / `noticeCount` are not declared here, so zod strips them; why
|
|
569
|
-
* they are not needed is on `projectSiteInfo`.
|
|
570
|
-
*/
|
|
571
|
-
declare const siteInfoResponseSchema: z.ZodObject<{
|
|
572
|
-
success: z.ZodLiteral<true>;
|
|
573
|
-
receptionHours: z.ZodObject<{
|
|
574
|
-
et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
575
|
-
en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
576
|
-
}, "strip", z.ZodTypeAny, {
|
|
577
|
-
et?: string | null | undefined;
|
|
578
|
-
en?: string | null | undefined;
|
|
579
|
-
}, {
|
|
580
|
-
et?: string | null | undefined;
|
|
581
|
-
en?: string | null | undefined;
|
|
582
|
-
}>;
|
|
583
|
-
notices: z.ZodArray<z.ZodObject<{
|
|
584
|
-
text: z.ZodObject<{
|
|
585
|
-
et: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
586
|
-
en: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
587
|
-
}, "strip", z.ZodTypeAny, {
|
|
588
|
-
et?: string | null | undefined;
|
|
589
|
-
en?: string | null | undefined;
|
|
590
|
-
}, {
|
|
591
|
-
et?: string | null | undefined;
|
|
592
|
-
en?: string | null | undefined;
|
|
593
|
-
}>;
|
|
594
|
-
isActive: z.ZodBoolean;
|
|
595
|
-
order: z.ZodNumber;
|
|
596
|
-
}, "strip", z.ZodTypeAny, {
|
|
597
|
-
text: {
|
|
598
|
-
et?: string | null | undefined;
|
|
599
|
-
en?: string | null | undefined;
|
|
600
|
-
};
|
|
601
|
-
order: number;
|
|
602
|
-
isActive: boolean;
|
|
603
|
-
}, {
|
|
604
|
-
text: {
|
|
605
|
-
et?: string | null | undefined;
|
|
606
|
-
en?: string | null | undefined;
|
|
607
|
-
};
|
|
608
|
-
order: number;
|
|
609
|
-
isActive: boolean;
|
|
610
|
-
}>, "many">;
|
|
611
|
-
}, "strip", z.ZodTypeAny, {
|
|
612
|
-
receptionHours: {
|
|
613
|
-
et?: string | null | undefined;
|
|
614
|
-
en?: string | null | undefined;
|
|
615
|
-
};
|
|
616
|
-
notices: {
|
|
617
|
-
text: {
|
|
618
|
-
et?: string | null | undefined;
|
|
619
|
-
en?: string | null | undefined;
|
|
620
|
-
};
|
|
621
|
-
order: number;
|
|
622
|
-
isActive: boolean;
|
|
623
|
-
}[];
|
|
624
|
-
success: true;
|
|
625
|
-
}, {
|
|
626
|
-
receptionHours: {
|
|
627
|
-
et?: string | null | undefined;
|
|
628
|
-
en?: string | null | undefined;
|
|
629
|
-
};
|
|
630
|
-
notices: {
|
|
631
|
-
text: {
|
|
632
|
-
et?: string | null | undefined;
|
|
633
|
-
en?: string | null | undefined;
|
|
634
|
-
};
|
|
635
|
-
order: number;
|
|
636
|
-
isActive: boolean;
|
|
637
|
-
}[];
|
|
638
|
-
success: true;
|
|
639
|
-
}>;
|
|
640
|
-
type SiteInfoResponse = z.infer<typeof siteInfoResponseSchema>;
|
|
641
|
-
//#endregion
|
|
642
|
-
//#region src/projection.d.ts
|
|
643
|
-
/** Injected at the seam; see the module docblock for why it is not imported. */
|
|
644
|
-
type HtmlSanitizer = (html: string) => string;
|
|
645
|
-
interface ProjectionOptions {
|
|
646
|
-
/** The locale whose values live on the base entity row. `et` or `en`. */
|
|
647
|
-
readonly defaultLocale: string;
|
|
648
|
-
}
|
|
649
|
-
interface ProjectedRow {
|
|
650
|
-
/** The business key — `room_type.code` or `legal_document.type`. */
|
|
651
|
-
readonly key: string;
|
|
652
|
-
/** Base-row fields, including the default locale's values for translatable fields. */
|
|
653
|
-
readonly base: Record<string, unknown>;
|
|
654
|
-
/**
|
|
655
|
-
* Translatable values for the non-default locale, or `null` when that
|
|
656
|
-
* locale's text is empty upstream. `null` means "unpublish that locale",
|
|
657
|
-
* never "write an empty string".
|
|
658
|
-
*/
|
|
659
|
-
readonly secondary: Record<string, unknown> | null;
|
|
660
|
-
readonly hash: string;
|
|
661
|
-
}
|
|
662
|
-
interface SkippedRow {
|
|
663
|
-
readonly key: string;
|
|
664
|
-
readonly reason: string;
|
|
665
|
-
}
|
|
666
|
-
interface ProjectionResult {
|
|
667
|
-
readonly projected: readonly ProjectedRow[];
|
|
668
|
-
readonly skipped: readonly SkippedRow[];
|
|
669
|
-
}
|
|
670
|
-
/** The locale that is NOT the base row's. */
|
|
671
|
-
declare function secondaryLocaleOf(defaultLocale: string): string;
|
|
672
|
-
declare function projectRoomTypes(rows: readonly RoomTypeRow[], options: ProjectionOptions): ProjectionResult;
|
|
673
|
-
declare function projectLegalDocuments(rows: readonly LegalDocumentRow[], options: ProjectionOptions, sanitize: HtmlSanitizer): ProjectionResult;
|
|
674
|
-
/**
|
|
675
|
-
* Reception hours + the notice ticker → the one `yhikas_site_info` row.
|
|
676
|
-
*
|
|
677
|
-
* ## 🔴 The refusal is the point of this function
|
|
678
|
-
*
|
|
679
|
-
* `/api/public/site-info` answers `200 { success: true, receptionHours:
|
|
680
|
-
* {et:'',en:''}, notices: [] }` when no upstream row exists. That is
|
|
681
|
-
* byte-identical to an operator having deliberately cleared both, and it is the
|
|
682
|
-
* state the endpoint is in whenever nobody has filled it in (F015). The other
|
|
683
|
-
* two resources have no equivalent hole — theirs discriminate on `success` and
|
|
684
|
-
* on array length against a local snapshot.
|
|
685
|
-
*
|
|
686
|
-
* The shipped `empty-snapshot` floor cannot cover it, and the reason is
|
|
687
|
-
* structural rather than an oversight: that rule is a row-COUNT test, and this
|
|
688
|
-
* resource always projects exactly one row. The count is 1 whether the row says
|
|
689
|
-
* anything or not, so the floor never fires and an all-empty payload would be
|
|
690
|
-
* applied as an authoritative blanking — emptying the ticker and the hours on
|
|
691
|
-
* every page of the site, silently, six hours after upstream hiccupped.
|
|
692
|
-
*
|
|
693
|
-
* So emptiness is restated here as a CONTENT test: if the default locale has
|
|
694
|
-
* neither hours nor a single active notice, the payload is refused as
|
|
695
|
-
* unusable. Local content is left exactly as it was and the failure arms the
|
|
696
|
-
* staleness watchdog, which is the same posture as an unreachable endpoint —
|
|
697
|
-
* because epistemically it is the same situation.
|
|
698
|
-
*
|
|
699
|
-
* Note what is NOT refused: hours with no notices, or notices with no hours.
|
|
700
|
-
* Both are ordinary states of a real dormitory, and refusing them would make
|
|
701
|
-
* the guard fire on exactly the operator action it exists to protect.
|
|
702
|
-
*
|
|
703
|
-
* ## Why upstream's `updatedAt: null` is not read (F015, decided 2026-09-29)
|
|
704
|
-
*
|
|
705
|
-
* yhikas-admin now answers `updatedAt: null` when both of its tables are
|
|
706
|
-
* empty, which looks like the missing "never configured" signal. It adds
|
|
707
|
-
* nothing this refusal does not already know. Upstream enforces a database
|
|
708
|
-
* CHECK that every `site_info.receptionHours` from a row has both locales
|
|
709
|
-
* non-blank; the empty `{et:'',en:''}` is a value the route FABRICATES when no
|
|
710
|
-
* row exists (its `PUBLIC_API.md`, "The one exception"). So once the
|
|
711
|
-
* constraints are deployed, "deliberately cleared" is not a state an operator
|
|
712
|
-
* can produce: empty hours can only mean no row, and every all-empty response
|
|
713
|
-
* is already "never configured". Reading the null would split one case into
|
|
714
|
-
* one case. It would also make the guard depend on a field an older
|
|
715
|
-
* deployment omits — an absent key, not an error. So the content test stays
|
|
716
|
-
* the single rule, and it holds whether or not the field is present.
|
|
717
|
-
*/
|
|
718
|
-
declare function projectSiteInfo(response: SiteInfoResponse, options: ProjectionOptions): ProjectionResult;
|
|
719
|
-
//#endregion
|
|
720
|
-
//#region src/apply.d.ts
|
|
721
|
-
/**
|
|
722
|
-
* The `AdminClient` surface the sync uses, structurally.
|
|
723
|
-
*
|
|
724
|
-
* Declared rather than imported so the apply logic is unit-testable with a
|
|
725
|
-
* fake — CI runs unit tests only, with no database, so a design that could
|
|
726
|
-
* only be exercised by an integration test would in practice be exercised by
|
|
727
|
-
* nothing.
|
|
728
|
-
*/
|
|
729
|
-
interface SyncEntityClient {
|
|
730
|
-
findMany(options: {
|
|
731
|
-
limit: number;
|
|
732
|
-
}): Promise<Record<string, unknown>[]>;
|
|
733
|
-
create(data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
734
|
-
update(id: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
735
|
-
updateForLocale(id: string, data: Record<string, unknown>, locale: string): Promise<Record<string, unknown>>;
|
|
736
|
-
deleteTranslation(id: string, locale: string): Promise<void>;
|
|
737
|
-
}
|
|
738
|
-
interface SyncLogger {
|
|
739
|
-
info(obj: Record<string, unknown>, msg: string): void;
|
|
740
|
-
warn(obj: Record<string, unknown>, msg: string): void;
|
|
741
|
-
error(obj: Record<string, unknown>, msg: string): void;
|
|
742
|
-
}
|
|
743
|
-
interface ApplyCounts {
|
|
744
|
-
created: number;
|
|
745
|
-
updated: number;
|
|
746
|
-
retired: number;
|
|
747
|
-
unchanged: number;
|
|
748
|
-
failed: number;
|
|
749
|
-
}
|
|
750
|
-
/** Thrown when at least one row failed; carries the counts that were achieved. */
|
|
751
|
-
declare class YhikasApplyPartialError extends Error {
|
|
752
|
-
readonly counts: ApplyCounts;
|
|
753
|
-
constructor(resource: SyncResource, counts: ApplyCounts);
|
|
754
|
-
}
|
|
755
|
-
declare function readLocalRows(client: SyncEntityClient, keyField: string, limit: number): Promise<LocalRow[]>;
|
|
756
|
-
interface ApplyPlanInput {
|
|
757
|
-
readonly resource: SyncResource;
|
|
758
|
-
readonly plan: DiffPlan<ProjectedRow>;
|
|
759
|
-
readonly client: SyncEntityClient;
|
|
760
|
-
/** The locale that is NOT on the base row — the one whose publish state is toggled. */
|
|
761
|
-
readonly secondaryLocale: string;
|
|
762
|
-
readonly logger: SyncLogger;
|
|
763
|
-
}
|
|
764
|
-
declare function applyPlan(input: ApplyPlanInput): Promise<ApplyCounts>;
|
|
765
|
-
//#endregion
|
|
766
7
|
//#region src/entities/guard.d.ts
|
|
767
8
|
/** Thrown when a writer other than the sync tries to mutate synced content. */
|
|
768
9
|
declare class YhikasSyncedContentReadOnlyError extends Error {
|
|
@@ -776,132 +17,6 @@ declare class YhikasSyncedContentReadOnlyError extends Error {
|
|
|
776
17
|
*/
|
|
777
18
|
declare function machineWritten<N extends string>(entityName: N): Behavior<Record<never, never>, `yhikas-sync:machine-written:${N}`>;
|
|
778
19
|
//#endregion
|
|
779
|
-
//#region src/entities/index.d.ts
|
|
780
|
-
/**
|
|
781
|
-
* The entities this plugin syncs — ONE list, consumed by the plugin's
|
|
782
|
-
* declaration and by the contract tests.
|
|
783
|
-
*
|
|
784
|
-
* Adding `site_info` turned twelve tests red across two files, every one of
|
|
785
|
-
* them because a resource or entity had been re-listed by hand somewhere. This
|
|
786
|
-
* export exists so the next one cannot: `plugin.ts` spreads it into
|
|
787
|
-
* `server.entities`, and `contract.test.ts` iterates it, so a fourth entity is
|
|
788
|
-
* covered by both the moment it is added here.
|
|
789
|
-
*/
|
|
790
|
-
declare const SYNCED_ENTITIES: readonly [import("@murumets-ee/entity").Entity<{
|
|
791
|
-
id: import("@murumets-ee/entity").IdField;
|
|
792
|
-
} & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
|
|
793
|
-
code: import("@murumets-ee/entity").TextField & {
|
|
794
|
-
readonly required: true;
|
|
795
|
-
readonly unique: true;
|
|
796
|
-
readonly indexed: true;
|
|
797
|
-
readonly maxLength: 190;
|
|
798
|
-
};
|
|
799
|
-
name: import("@murumets-ee/entity").TextField & {
|
|
800
|
-
readonly required: true;
|
|
801
|
-
readonly translatable: true;
|
|
802
|
-
};
|
|
803
|
-
totalArea: import("@murumets-ee/entity").TextField & {
|
|
804
|
-
readonly maxLength: 32;
|
|
805
|
-
};
|
|
806
|
-
livingArea: import("@murumets-ee/entity").TextField & {
|
|
807
|
-
readonly maxLength: 32;
|
|
808
|
-
};
|
|
809
|
-
commonArea: import("@murumets-ee/entity").TextField & {
|
|
810
|
-
readonly maxLength: 32;
|
|
811
|
-
};
|
|
812
|
-
capacity: import("@murumets-ee/entity").NumberField & {
|
|
813
|
-
readonly integer: true;
|
|
814
|
-
};
|
|
815
|
-
placesOccupied: import("@murumets-ee/entity").NumberField & {
|
|
816
|
-
readonly integer: true;
|
|
817
|
-
};
|
|
818
|
-
monthlyRent: import("@murumets-ee/entity").TextField & {
|
|
819
|
-
readonly maxLength: 32;
|
|
820
|
-
};
|
|
821
|
-
discountedMonthlyRent: import("@murumets-ee/entity").TextField & {
|
|
822
|
-
readonly maxLength: 32;
|
|
823
|
-
};
|
|
824
|
-
dailyRent: import("@murumets-ee/entity").TextField & {
|
|
825
|
-
readonly maxLength: 32;
|
|
826
|
-
};
|
|
827
|
-
currency: import("@murumets-ee/entity").TextField & {
|
|
828
|
-
readonly required: true;
|
|
829
|
-
readonly maxLength: 3;
|
|
830
|
-
readonly default: "EUR";
|
|
831
|
-
};
|
|
832
|
-
vatTreatment: import("@murumets-ee/entity").SelectField & {
|
|
833
|
-
options: readonly ["net", "gross"];
|
|
834
|
-
} & {
|
|
835
|
-
readonly options: readonly ["net", "gross"];
|
|
836
|
-
readonly required: true;
|
|
837
|
-
readonly default: "net";
|
|
838
|
-
};
|
|
839
|
-
hasEnglish: import("@murumets-ee/entity").BooleanField & {
|
|
840
|
-
readonly required: true;
|
|
841
|
-
readonly default: false;
|
|
842
|
-
};
|
|
843
|
-
sourceHash: import("@murumets-ee/entity").TextField & {
|
|
844
|
-
readonly maxLength: 64;
|
|
845
|
-
};
|
|
846
|
-
}, "yhikas_room_type", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_room_type">]>, import("@murumets-ee/entity").Entity<{
|
|
847
|
-
id: import("@murumets-ee/entity").IdField;
|
|
848
|
-
} & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
|
|
849
|
-
type: import("@murumets-ee/entity").TextField & {
|
|
850
|
-
readonly required: true;
|
|
851
|
-
readonly unique: true;
|
|
852
|
-
readonly indexed: true;
|
|
853
|
-
readonly maxLength: 190;
|
|
854
|
-
};
|
|
855
|
-
title: import("@murumets-ee/entity").TextField & {
|
|
856
|
-
readonly required: true;
|
|
857
|
-
readonly translatable: true;
|
|
858
|
-
};
|
|
859
|
-
sourceSlug: import("@murumets-ee/entity").TextField & {
|
|
860
|
-
readonly required: true;
|
|
861
|
-
readonly indexed: true;
|
|
862
|
-
readonly maxLength: 190;
|
|
863
|
-
};
|
|
864
|
-
body: import("@murumets-ee/entity").RichTextField & {
|
|
865
|
-
readonly required: true;
|
|
866
|
-
readonly translatable: true;
|
|
867
|
-
};
|
|
868
|
-
order: import("@murumets-ee/entity").NumberField & {
|
|
869
|
-
readonly integer: true;
|
|
870
|
-
readonly required: true;
|
|
871
|
-
readonly default: 0;
|
|
872
|
-
};
|
|
873
|
-
hasEnglish: import("@murumets-ee/entity").BooleanField & {
|
|
874
|
-
readonly required: true;
|
|
875
|
-
readonly default: false;
|
|
876
|
-
};
|
|
877
|
-
sourceHash: import("@murumets-ee/entity").TextField & {
|
|
878
|
-
readonly maxLength: 64;
|
|
879
|
-
};
|
|
880
|
-
}, "yhikas_legal_document", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_legal_document">]>, import("@murumets-ee/entity").Entity<{
|
|
881
|
-
id: import("@murumets-ee/entity").IdField;
|
|
882
|
-
} & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
|
|
883
|
-
key: import("@murumets-ee/entity").TextField & {
|
|
884
|
-
readonly required: true;
|
|
885
|
-
readonly unique: true;
|
|
886
|
-
readonly indexed: true;
|
|
887
|
-
readonly maxLength: 32;
|
|
888
|
-
};
|
|
889
|
-
receptionHours: import("@murumets-ee/entity").TextField & {
|
|
890
|
-
readonly translatable: true;
|
|
891
|
-
readonly maxLength: 500;
|
|
892
|
-
};
|
|
893
|
-
notices: import("@murumets-ee/entity").JsonField & {
|
|
894
|
-
readonly translatable: true;
|
|
895
|
-
};
|
|
896
|
-
hasEnglish: import("@murumets-ee/entity").BooleanField & {
|
|
897
|
-
readonly default: false;
|
|
898
|
-
readonly required: true;
|
|
899
|
-
};
|
|
900
|
-
sourceHash: import("@murumets-ee/entity").TextField & {
|
|
901
|
-
readonly maxLength: 64;
|
|
902
|
-
};
|
|
903
|
-
}, "yhikas_site_info", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_site_info">]>];
|
|
904
|
-
//#endregion
|
|
905
20
|
//#region src/entities/legal-document.d.ts
|
|
906
21
|
/**
|
|
907
22
|
* `yhikas_legal_document` — the local projection of yhikas-admin's
|
|
@@ -1118,6 +233,122 @@ declare const YhikasRoomType: import("@murumets-ee/entity").Entity<{
|
|
|
1118
233
|
};
|
|
1119
234
|
}, "yhikas_room_type", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_room_type">]>;
|
|
1120
235
|
//#endregion
|
|
236
|
+
//#region src/entities/index.d.ts
|
|
237
|
+
declare const SYNCED_ENTITIES: readonly [import("@murumets-ee/entity").Entity<{
|
|
238
|
+
id: import("@murumets-ee/entity").IdField;
|
|
239
|
+
} & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
|
|
240
|
+
code: import("@murumets-ee/entity").TextField & {
|
|
241
|
+
readonly required: true;
|
|
242
|
+
readonly unique: true;
|
|
243
|
+
readonly indexed: true;
|
|
244
|
+
readonly maxLength: 190;
|
|
245
|
+
};
|
|
246
|
+
name: import("@murumets-ee/entity").TextField & {
|
|
247
|
+
readonly required: true;
|
|
248
|
+
readonly translatable: true;
|
|
249
|
+
};
|
|
250
|
+
totalArea: import("@murumets-ee/entity").TextField & {
|
|
251
|
+
readonly maxLength: 32;
|
|
252
|
+
};
|
|
253
|
+
livingArea: import("@murumets-ee/entity").TextField & {
|
|
254
|
+
readonly maxLength: 32;
|
|
255
|
+
};
|
|
256
|
+
commonArea: import("@murumets-ee/entity").TextField & {
|
|
257
|
+
readonly maxLength: 32;
|
|
258
|
+
};
|
|
259
|
+
capacity: import("@murumets-ee/entity").NumberField & {
|
|
260
|
+
readonly integer: true;
|
|
261
|
+
};
|
|
262
|
+
placesOccupied: import("@murumets-ee/entity").NumberField & {
|
|
263
|
+
readonly integer: true;
|
|
264
|
+
};
|
|
265
|
+
monthlyRent: import("@murumets-ee/entity").TextField & {
|
|
266
|
+
readonly maxLength: 32;
|
|
267
|
+
};
|
|
268
|
+
discountedMonthlyRent: import("@murumets-ee/entity").TextField & {
|
|
269
|
+
readonly maxLength: 32;
|
|
270
|
+
};
|
|
271
|
+
dailyRent: import("@murumets-ee/entity").TextField & {
|
|
272
|
+
readonly maxLength: 32;
|
|
273
|
+
};
|
|
274
|
+
currency: import("@murumets-ee/entity").TextField & {
|
|
275
|
+
readonly required: true;
|
|
276
|
+
readonly maxLength: 3;
|
|
277
|
+
readonly default: "EUR";
|
|
278
|
+
};
|
|
279
|
+
vatTreatment: import("@murumets-ee/entity").SelectField & {
|
|
280
|
+
options: readonly ["net", "gross"];
|
|
281
|
+
} & {
|
|
282
|
+
readonly options: readonly ["net", "gross"];
|
|
283
|
+
readonly required: true;
|
|
284
|
+
readonly default: "net";
|
|
285
|
+
};
|
|
286
|
+
hasEnglish: import("@murumets-ee/entity").BooleanField & {
|
|
287
|
+
readonly required: true;
|
|
288
|
+
readonly default: false;
|
|
289
|
+
};
|
|
290
|
+
sourceHash: import("@murumets-ee/entity").TextField & {
|
|
291
|
+
readonly maxLength: 64;
|
|
292
|
+
};
|
|
293
|
+
}, "yhikas_room_type", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_room_type">]>, import("@murumets-ee/entity").Entity<{
|
|
294
|
+
id: import("@murumets-ee/entity").IdField;
|
|
295
|
+
} & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
|
|
296
|
+
type: import("@murumets-ee/entity").TextField & {
|
|
297
|
+
readonly required: true;
|
|
298
|
+
readonly unique: true;
|
|
299
|
+
readonly indexed: true;
|
|
300
|
+
readonly maxLength: 190;
|
|
301
|
+
};
|
|
302
|
+
title: import("@murumets-ee/entity").TextField & {
|
|
303
|
+
readonly required: true;
|
|
304
|
+
readonly translatable: true;
|
|
305
|
+
};
|
|
306
|
+
sourceSlug: import("@murumets-ee/entity").TextField & {
|
|
307
|
+
readonly required: true;
|
|
308
|
+
readonly indexed: true;
|
|
309
|
+
readonly maxLength: 190;
|
|
310
|
+
};
|
|
311
|
+
body: import("@murumets-ee/entity").RichTextField & {
|
|
312
|
+
readonly required: true;
|
|
313
|
+
readonly translatable: true;
|
|
314
|
+
};
|
|
315
|
+
order: import("@murumets-ee/entity").NumberField & {
|
|
316
|
+
readonly integer: true;
|
|
317
|
+
readonly required: true;
|
|
318
|
+
readonly default: 0;
|
|
319
|
+
};
|
|
320
|
+
hasEnglish: import("@murumets-ee/entity").BooleanField & {
|
|
321
|
+
readonly required: true;
|
|
322
|
+
readonly default: false;
|
|
323
|
+
};
|
|
324
|
+
sourceHash: import("@murumets-ee/entity").TextField & {
|
|
325
|
+
readonly maxLength: 64;
|
|
326
|
+
};
|
|
327
|
+
}, "yhikas_legal_document", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_legal_document">]>, import("@murumets-ee/entity").Entity<{
|
|
328
|
+
id: import("@murumets-ee/entity").IdField;
|
|
329
|
+
} & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
|
|
330
|
+
key: import("@murumets-ee/entity").TextField & {
|
|
331
|
+
readonly required: true;
|
|
332
|
+
readonly unique: true;
|
|
333
|
+
readonly indexed: true;
|
|
334
|
+
readonly maxLength: 32;
|
|
335
|
+
};
|
|
336
|
+
receptionHours: import("@murumets-ee/entity").TextField & {
|
|
337
|
+
readonly translatable: true;
|
|
338
|
+
readonly maxLength: 500;
|
|
339
|
+
};
|
|
340
|
+
notices: import("@murumets-ee/entity").JsonField & {
|
|
341
|
+
readonly translatable: true;
|
|
342
|
+
};
|
|
343
|
+
hasEnglish: import("@murumets-ee/entity").BooleanField & {
|
|
344
|
+
readonly default: false;
|
|
345
|
+
readonly required: true;
|
|
346
|
+
};
|
|
347
|
+
sourceHash: import("@murumets-ee/entity").TextField & {
|
|
348
|
+
readonly maxLength: 64;
|
|
349
|
+
};
|
|
350
|
+
}, "yhikas_site_info", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_site_info">]>];
|
|
351
|
+
//#endregion
|
|
1121
352
|
//#region src/entities/site-info.d.ts
|
|
1122
353
|
/**
|
|
1123
354
|
* `yhikas_site_info` — reception hours and the notice ticker, as ONE row.
|
|
@@ -1208,18 +439,105 @@ declare const YhikasSiteInfo: import("@murumets-ee/entity").Entity<{
|
|
|
1208
439
|
};
|
|
1209
440
|
}, "yhikas_site_info", [import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").Behavior<import("@murumets-ee/entity").PublishableFields, "publishable">, import("@murumets-ee/entity").Behavior<Record<never, never>, "yhikas-sync:machine-written:yhikas_site_info">]>;
|
|
1210
441
|
//#endregion
|
|
1211
|
-
//#region src/
|
|
1212
|
-
/** The
|
|
1213
|
-
|
|
442
|
+
//#region src/room-pages.d.ts
|
|
443
|
+
/** The most pages one run creates. The rest are created on later runs. */
|
|
444
|
+
declare const MAX_ROOM_PAGES_PER_RUN = 20;
|
|
445
|
+
/** The page entity, as the drafts read and write it. */
|
|
446
|
+
interface RoomPageWriter {
|
|
447
|
+
/** Pages whose default-locale slug is `slug`, at most `limit`. */
|
|
448
|
+
findBySlug(slug: string, limit: number): Promise<readonly {
|
|
449
|
+
readonly id: string;
|
|
450
|
+
readonly parent: string | null;
|
|
451
|
+
}[]>;
|
|
452
|
+
/** The children of `parentId` whose default-locale slug is one of `slugs`: slug → page id. */
|
|
453
|
+
findChildren(parentId: string, slugs: readonly string[]): Promise<ReadonlyMap<string, string>>;
|
|
454
|
+
/** Create a DRAFT page in the default locale; answers its id. */
|
|
455
|
+
createDraft(page: {
|
|
456
|
+
readonly slug: string;
|
|
457
|
+
readonly parentId: string;
|
|
458
|
+
readonly title: string;
|
|
459
|
+
readonly layout: RoomPageDraft['layout'];
|
|
460
|
+
}): Promise<string>;
|
|
461
|
+
/** Write one non-default locale's title, tree and slug. Never touches status. */
|
|
462
|
+
writeLocale(id: string, locale: string, page: {
|
|
463
|
+
readonly slug: string;
|
|
464
|
+
readonly title: string;
|
|
465
|
+
readonly layout: RoomPageDraft['layout'];
|
|
466
|
+
}): Promise<void>;
|
|
467
|
+
}
|
|
468
|
+
/** The synced room types, as the drafts read them. */
|
|
469
|
+
interface RoomTypeReader {
|
|
470
|
+
/**
|
|
471
|
+
* PUBLISHED room types in `locale` (name in that locale), at most `limit`;
|
|
472
|
+
* only those with a code in `codes` when it is given.
|
|
473
|
+
*/
|
|
474
|
+
published(locale: string, options: {
|
|
475
|
+
readonly limit: number;
|
|
476
|
+
readonly codes?: readonly string[];
|
|
477
|
+
}): Promise<readonly RoomTypeForPage[]>;
|
|
478
|
+
/** Which of these row ids are room types, published or not. Asked only about a path conflict. */
|
|
479
|
+
existingIds(ids: readonly string[]): Promise<ReadonlySet<string>>;
|
|
480
|
+
}
|
|
481
|
+
interface RoomPageDraftDeps {
|
|
482
|
+
readonly config: ResolvedRoomPagesConfig;
|
|
483
|
+
readonly rooms: RoomTypeReader;
|
|
484
|
+
readonly pages: RoomPageWriter;
|
|
485
|
+
readonly registry: RoomPageRegistry;
|
|
486
|
+
readonly logger: SyncLogger;
|
|
487
|
+
/** The app's real default locale — the base row's language. */
|
|
488
|
+
readonly defaultLocale: string;
|
|
489
|
+
/** Every enabled content locale, the default included. */
|
|
490
|
+
readonly locales: readonly string[];
|
|
491
|
+
/** The most room types read in one query — the sync's snapshot ceiling (`maxRowsPerResource`). */
|
|
492
|
+
readonly maxRows: number;
|
|
493
|
+
readonly now?: () => Date;
|
|
494
|
+
/** This run's claim token. Default: a random UUID. */
|
|
495
|
+
readonly token?: string;
|
|
496
|
+
/** Override {@link MAX_ROOM_PAGES_PER_RUN} — tests only. */
|
|
497
|
+
readonly maxPerRun?: number;
|
|
498
|
+
}
|
|
499
|
+
interface RoomPageSkip {
|
|
500
|
+
readonly code: string;
|
|
501
|
+
readonly reason: string;
|
|
502
|
+
}
|
|
503
|
+
interface RoomPageDraftSummary {
|
|
504
|
+
/** Codes a draft page was created for this run. */
|
|
505
|
+
readonly created: readonly string[];
|
|
506
|
+
/** Codes whose page already existed under the parent, now recorded. */
|
|
507
|
+
readonly found: readonly string[];
|
|
508
|
+
/**
|
|
509
|
+
* Codes whose page a CRASHED run created before recording it — found by
|
|
510
|
+
* slug with that run's claim still on the row, and recorded as `created`
|
|
511
|
+
* so the dashboard still lists them as waiting.
|
|
512
|
+
*/
|
|
513
|
+
readonly recovered: readonly string[];
|
|
514
|
+
/** Refused codes left out of this run because their last refusal is too recent. */
|
|
515
|
+
readonly backedOff: number;
|
|
516
|
+
/** Codes not acted on this run, with why. Retried next run. */
|
|
517
|
+
readonly skipped: readonly RoomPageSkip[];
|
|
518
|
+
/** Codes over the per-run cap, left for the next run. */
|
|
519
|
+
readonly deferred: number;
|
|
520
|
+
/** Non-default locale writes that failed (`code:locale`) — the page exists and stays recorded. */
|
|
521
|
+
readonly localeFailures: readonly string[];
|
|
522
|
+
/** Why nothing was done at all, when nothing was. */
|
|
523
|
+
readonly halted?: 'nothing-pending' | 'no-parent' | 'ambiguous-parent';
|
|
524
|
+
}
|
|
525
|
+
/** A code as a page slug: lowercased; `undefined` when it cannot be one. */
|
|
526
|
+
declare function roomPageSlug(code: string): string | undefined;
|
|
527
|
+
/**
|
|
528
|
+
* Why the page entity refused a room's page, for the dashboard. A path held by
|
|
529
|
+
* a `yhikas_room_type` row is named as such, because its fix is specific: the
|
|
530
|
+
* room type's OLD URL row, left in the path index from before `roomPages`,
|
|
531
|
+
* still holds the address the page needs.
|
|
532
|
+
*/
|
|
533
|
+
declare function classifyRefusal(err: unknown, rooms: RoomTypeReader): Promise<RoomPageRefusal>;
|
|
1214
534
|
/**
|
|
1215
|
-
*
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1219
|
-
* the context it builds, and `updateForLocale` THROWS when it can resolve
|
|
1220
|
-
* the default locale from neither the options nor the context.
|
|
535
|
+
* Create the missing draft pages. Throws only for a failure of its own reads
|
|
536
|
+
* (rooms, registry, the parent lookup) — the caller logs it; per-room failures
|
|
537
|
+
* are caught, logged, kept on the registry row for the dashboard and reported
|
|
538
|
+
* in the summary.
|
|
1221
539
|
*/
|
|
1222
|
-
declare function
|
|
540
|
+
declare function createRoomPageDrafts(deps: RoomPageDraftDeps): Promise<RoomPageDraftSummary>;
|
|
1223
541
|
//#endregion
|
|
1224
542
|
//#region src/sync-state-table.d.ts
|
|
1225
543
|
/**
|
|
@@ -1366,6 +684,141 @@ type SyncStateClient = ReturnType<typeof yhikasSyncStateTable.makeClient>;
|
|
|
1366
684
|
declare function truncateError(message: string): string;
|
|
1367
685
|
declare function createSyncStateStore(client: SyncStateClient): SyncStateStore;
|
|
1368
686
|
//#endregion
|
|
687
|
+
//#region src/room-facts.d.ts
|
|
688
|
+
/** The answer `resolve` gives — a record of facts per room type code. */
|
|
689
|
+
type LiveAnswer = Awaited<ReturnType<LiveSourceSpec['resolve']>>;
|
|
690
|
+
/** The fact names this source answers. */
|
|
691
|
+
declare const ROOM_FACT_NAMES: readonly ["monthly", "discounted", "daily", "places", "area"];
|
|
692
|
+
type RoomFactName = (typeof ROOM_FACT_NAMES)[number];
|
|
693
|
+
/** What a room answers — declared, so the panel offers them as checkboxes. */
|
|
694
|
+
declare const ROOM_FACTS: readonly LiveFactSpec[];
|
|
695
|
+
/** The library field a facts source is granted by default — `liveFacts.values`. */
|
|
696
|
+
declare const DEFAULT_ROOM_FACT_FIELDS: readonly string[];
|
|
697
|
+
/**
|
|
698
|
+
* A stored decimal string as a number, or `undefined` — never `NaN`, never a
|
|
699
|
+
* negative, never a fabricated 0 for a missing value.
|
|
700
|
+
*/
|
|
701
|
+
declare function decimalOf(raw: string | null): number | undefined;
|
|
702
|
+
/** One room type's facts — only the values it actually has. */
|
|
703
|
+
declare function roomFactsOf(room: RoomTypeForPage): Partial<Record<RoomFactName, number>>;
|
|
704
|
+
/** The pure half of `resolve`: the answer for `keys`, given the published rows. Unknown keys are absent. */
|
|
705
|
+
declare function roomFactsFrom(rooms: readonly RoomTypeForPage[], keys: readonly string[]): LiveAnswer;
|
|
706
|
+
/** The picker's items: offered rooms, by code, at most {@link MAX_ITEMS}. */
|
|
707
|
+
declare function roomFactItemsFrom(rooms: readonly RoomTypeForPage[], offered: RoomOfferPredicate): readonly LiveItem[];
|
|
708
|
+
/**
|
|
709
|
+
* A queue schedule as English words — `'@every 6h'` → `'every 6 hours'` —
|
|
710
|
+
* plus a translation key when the catalogue has one for it. A cron expression
|
|
711
|
+
* is named as such: its meaning is not worth a parser here.
|
|
712
|
+
*/
|
|
713
|
+
declare function cadenceOf(schedule: string): {
|
|
714
|
+
readonly text: string;
|
|
715
|
+
readonly key?: string;
|
|
716
|
+
};
|
|
717
|
+
interface YhikasRoomFactsOptions {
|
|
718
|
+
/** The source's English name in the panel. Default `'Room facts'`. */
|
|
719
|
+
readonly label?: string;
|
|
720
|
+
/**
|
|
721
|
+
* yhikas-admin's PUBLIC `https:` base URL — the panel links to
|
|
722
|
+
* `${adminUrl}/admin/toad`, where room prices are changed (yhikas-admin has
|
|
723
|
+
* no page per room). Omitted: no link.
|
|
724
|
+
*/
|
|
725
|
+
readonly adminUrl?: string;
|
|
726
|
+
/**
|
|
727
|
+
* The sync's schedule, for "synced every 6 hours". Default: the registered
|
|
728
|
+
* sync plugin's own `schedule` — read when the panel asks, so the provenance
|
|
729
|
+
* cannot drift from the job that actually runs — else `@every 6h`.
|
|
730
|
+
*/
|
|
731
|
+
readonly schedule?: string;
|
|
732
|
+
/**
|
|
733
|
+
* The staleness window. Default: the registered sync plugin's own
|
|
734
|
+
* `staleAfterMs`, else 24 h — so the facts go stale exactly when the
|
|
735
|
+
* watchdog alerts.
|
|
736
|
+
*/
|
|
737
|
+
readonly staleAfterMs?: number;
|
|
738
|
+
/** Which rooms the picker lists. Default: the sync's `roomPages.offered`, else {@link isRoomOffer}. */
|
|
739
|
+
readonly offered?: RoomOfferPredicate;
|
|
740
|
+
/**
|
|
741
|
+
* Library fields this source may also be bound to — see
|
|
742
|
+
* `LiveSourceSpec.fields`. Default {@link DEFAULT_ROOM_FACT_FIELDS}: the
|
|
743
|
+
* library Facts block's `values`, so a site binds a room page's facts with
|
|
744
|
+
* no further wiring. Pass `[]` to grant none.
|
|
745
|
+
*/
|
|
746
|
+
readonly fields?: readonly string[];
|
|
747
|
+
}
|
|
748
|
+
/** The part of the registered sync plugin's config the facts read. */
|
|
749
|
+
interface RegisteredSyncConfig {
|
|
750
|
+
readonly schedule: string;
|
|
751
|
+
readonly staleAfterMs: number;
|
|
752
|
+
readonly roomPages?: {
|
|
753
|
+
readonly offered: RoomOfferPredicate;
|
|
754
|
+
} | undefined;
|
|
755
|
+
}
|
|
756
|
+
/** What the factory reads at RUN time — injectable, so each part is testable without an app. */
|
|
757
|
+
interface RoomFactsRuntime {
|
|
758
|
+
/** Published room types through the caller's own client. */
|
|
759
|
+
readonly rooms: () => Promise<RoomTypeReader>;
|
|
760
|
+
/** The sync state, for freshness. */
|
|
761
|
+
readonly syncState: () => Promise<readonly SyncStateRecord[]>;
|
|
762
|
+
/**
|
|
763
|
+
* The registered sync plugin's resolved config, when it is registered.
|
|
764
|
+
* Synchronous: `provenance` is a plain property the canvas reads without
|
|
765
|
+
* awaiting.
|
|
766
|
+
*/
|
|
767
|
+
readonly syncConfig: () => RegisteredSyncConfig | undefined;
|
|
768
|
+
readonly now: () => Date;
|
|
769
|
+
}
|
|
770
|
+
/** Room types' freshness, from the sync state — stale past the window, or never synced. */
|
|
771
|
+
declare function roomTypesFreshness(records: readonly SyncStateRecord[], now: Date, staleAfterMs: number): LiveFreshness;
|
|
772
|
+
/**
|
|
773
|
+
* The synced room types as a live facts source. Keys are room type `code`s.
|
|
774
|
+
*
|
|
775
|
+
* Throws only for a structurally invalid `adminUrl` — at config load, where
|
|
776
|
+
* the message can name the knob.
|
|
777
|
+
*/
|
|
778
|
+
declare function yhikasRoomFacts(options?: YhikasRoomFactsOptions, runtime?: RoomFactsRuntime): LiveSourceSpec;
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/room-page-record.d.ts
|
|
781
|
+
/**
|
|
782
|
+
* `recordRoomPage(code, pageId)` — for a site's own migration or seeder that
|
|
783
|
+
* wrote a room's page itself: records that the room HAS its page, so the sync
|
|
784
|
+
* never creates a draft for it (`yhikas_room_pages`, origin `registered`).
|
|
785
|
+
*
|
|
786
|
+
* Never overwrites: a code already recorded keeps its first page. Server-side
|
|
787
|
+
* only — call it from a seeder or a `lumi` command, never from a route that
|
|
788
|
+
* forwards request input.
|
|
789
|
+
*/
|
|
790
|
+
/**
|
|
791
|
+
* @returns `true` when recorded; `false` when the code already has a page, or
|
|
792
|
+
* a sync run is creating one right now (its claim is fresh).
|
|
793
|
+
*/
|
|
794
|
+
declare function recordRoomPage(code: string, pageId: string): Promise<boolean>;
|
|
795
|
+
//#endregion
|
|
796
|
+
//#region src/room-pages-adapters.d.ts
|
|
797
|
+
/**
|
|
798
|
+
* One column of an entity table, by field name — or a loud error naming it.
|
|
799
|
+
*
|
|
800
|
+
* `getTable()` is typed `PgTableWithColumns<any>`: an entity's columns are
|
|
801
|
+
* generated at runtime, so their names are not in the type. Narrowed here with
|
|
802
|
+
* drizzle's own `is()` (which survives duplicated module instances, unlike
|
|
803
|
+
* `instanceof`) instead of trusting an `any`. For the page entity, `slug`
|
|
804
|
+
* comes from `sluggable()` and `parent` from `pathParent()`; a page entity
|
|
805
|
+
* without either cannot hold room pages, and says so.
|
|
806
|
+
*/
|
|
807
|
+
declare function columnOf(table: Readonly<Record<string, unknown>>, entity: string, field: string): PgColumn;
|
|
808
|
+
/**
|
|
809
|
+
* Published room types, read through the CALLER's client — the job's (as the
|
|
810
|
+
* sync actor) or a render's (as the visitor). An unpublished row — the sync's
|
|
811
|
+
* way of retiring a room — is never answered, here or by the status filter in
|
|
812
|
+
* SQL: both, because the reader is the one place a draft price could leak.
|
|
813
|
+
*/
|
|
814
|
+
declare function toRoomTypeReader(client: AdminClientLike, defaultLocale?: string): RoomTypeReader;
|
|
815
|
+
/**
|
|
816
|
+
* The page entity, for the drafts. Writes go through `AdminClient` — hooks,
|
|
817
|
+
* validation, the path guard, audit — as the job's own identity; nothing here
|
|
818
|
+
* elevates.
|
|
819
|
+
*/
|
|
820
|
+
declare function toRoomPageWriter(client: AdminClientLike, defaultLocale: string, logger?: SyncLogger): RoomPageWriter;
|
|
821
|
+
//#endregion
|
|
1369
822
|
//#region src/upstream/client.d.ts
|
|
1370
823
|
interface YhikasUpstreamClientOptions {
|
|
1371
824
|
/** Origin of the yhikas-admin deployment. Must be http or https. */
|
|
@@ -1408,6 +861,13 @@ interface SyncRunDeps {
|
|
|
1408
861
|
readonly floor: SanityFloor;
|
|
1409
862
|
/** Injectable so tests are not order-dependent on the wall clock. */
|
|
1410
863
|
readonly now?: () => Date;
|
|
864
|
+
/**
|
|
865
|
+
* Runs once, after every resource has been attempted, when ROOM TYPES
|
|
866
|
+
* synced successfully — the room-page drafts (`roomPages`). Whatever it
|
|
867
|
+
* throws is logged and swallowed: the prices are already current, and a page
|
|
868
|
+
* that cannot be written must never fail, retry or alert the sync.
|
|
869
|
+
*/
|
|
870
|
+
readonly afterRoomTypesSynced?: () => Promise<void>;
|
|
1411
871
|
}
|
|
1412
872
|
interface ResourceOutcome {
|
|
1413
873
|
readonly resource: SyncResource;
|
|
@@ -1499,5 +959,5 @@ declare function assessStaleness(records: readonly SyncStateRecord[], now: Date,
|
|
|
1499
959
|
*/
|
|
1500
960
|
declare function assertNotStale(records: readonly SyncStateRecord[], now: Date, staleAfterMs: number): ResourceStaleness[];
|
|
1501
961
|
//#endregion
|
|
1502
|
-
export { API_KEY_ENV_VAR, type AdminClientLike, type ApplyCounts, type ApplyPlanInput, BASE_URL_ENV_VAR, DECLARED_CURRENCY, DECLARED_VAT_TREATMENT, type DiffPlan, EN_LOCALE, ET_LOCALE, type HtmlSanitizer, LEGAL_DOCUMENTS_RESOURCE, type LegalDocumentRow, type LocalRow, MAX_SITE_NOTICES, type MultilingualText, type PlanDiffInput, type ProjectedRow, type ProjectionOptions, type ProjectionResult, ROOM_TYPES_RESOURCE, type ResolvedYhikasSyncConfig, type ResourceOutcome, type ResourceStaleness, type RoomTypeRow, SITE_INFO_KEY, SITE_INFO_RESOURCE, SYNCED_ENTITIES, SYNC_JOB_NAME, SYNC_RESOURCES, type SanityFloor, type SiteInfoResponse, type SiteNoticeRow, type SkippedRow, type SyncEntityClient, type SyncLogger, type SyncResource, type SyncRunDeps, type SyncRunSummary, type SyncStateRecord, type SyncStateStore, type UpstreamFailureKind, VAT_TREATMENTS, WATCHDOG_JOB_NAME, YHIKAS_LEGAL_DOCUMENT_ENTITY, YHIKAS_ROOM_TYPE_ENTITY, YHIKAS_SITE_INFO_ENTITY, YHIKAS_SYNC_ACTOR_ID, YHIKAS_SYNC_PLUGIN_NAME, YhikasApplyPartialError, YhikasLegalDocument, YhikasRoomType, YhikasSiteInfo, type YhikasSyncConfig, YhikasSyncConfigError, YhikasSyncRefusedError, YhikasSyncRunError, YhikasSyncStaleError, YhikasSyncedContentReadOnlyError, YhikasUpstreamClient, type YhikasUpstreamClientOptions, YhikasUpstreamError, applyPlan, assertNotStale, assessStaleness, createSyncStateStore, legalDocumentRowSchema, legalDocumentsResponseSchema, machineWritten, multilingualTextSchema, planDiff, projectLegalDocuments, projectRoomTypes, projectSiteInfo, readLocalRows, resolveYhikasSyncConfig, roomTypeRowSchema, roomTypesResponseSchema, runYhikasSync, secondaryLocaleOf, siteInfoResponseSchema, siteNoticeRowSchema, stableHash, toSyncEntityClient, truncateError, yhikasSyncStateTable };
|
|
962
|
+
export { API_KEY_ENV_VAR, AREA_PRICED_UNIT_NAMES, type AdminClientLike, type ApplyCounts, type ApplyPlanInput, BASE_URL_ENV_VAR, DECLARED_CURRENCY, DECLARED_VAT_TREATMENT, DEFAULT_ROOM_FACT_FIELDS, type DiffPlan, EN_LOCALE, ET_LOCALE, type HtmlSanitizer, LEGAL_DOCUMENTS_RESOURCE, type LegalDocumentRow, type LocalRow, MAX_ROOM_PAGES_PER_RUN, MAX_SITE_NOTICES, type MultilingualText, PAGE_SLUG_PATTERN, type PlanDiffInput, type ProjectedRow, type ProjectionOptions, type ProjectionResult, ROOM_FACTS, ROOM_FACT_NAMES, ROOM_PAGE_CLAIM_LEASE_MS, ROOM_PAGE_ORIGINS, ROOM_PAGE_READ_CHUNK, ROOM_PAGE_REFUSAL_KINDS, ROOM_TYPES_RESOURCE, type RegisteredSyncConfig, type ResolvedRoomPagesConfig, type ResolvedYhikasSyncConfig, type ResourceOutcome, type ResourceStaleness, type RoomFactName, type RoomFactsRuntime, type RoomOfferCandidate, type RoomOfferPredicate, type RoomPageBlock, type RoomPageDraft, type RoomPageDraftDeps, type RoomPageDraftSummary, type RoomPageOrigin, type RoomPageRecord, type RoomPageRefusal, type RoomPageRefusalKind, type RoomPageRegistry, type RoomPageSkip, type RoomPageWriter, type RoomTypeForPage, type RoomTypeReader, type RoomTypeRow, SITE_INFO_KEY, SITE_INFO_RESOURCE, SYNCED_ENTITIES, SYNC_JOB_NAME, SYNC_RESOURCES, type SanityFloor, type SiteInfoResponse, type SiteNoticeRow, type SkippedRow, type SyncEntityClient, type SyncLogger, type SyncResource, type SyncRunDeps, type SyncRunSummary, type SyncStateRecord, type SyncStateStore, type UpstreamFailureKind, VAT_TREATMENTS, WATCHDOG_JOB_NAME, YHIKAS_LEGAL_DOCUMENT_ENTITY, YHIKAS_ROOM_TYPE_ENTITY, YHIKAS_SITE_INFO_ENTITY, YHIKAS_SYNC_ACTOR_ID, YHIKAS_SYNC_PLUGIN_NAME, YhikasApplyPartialError, YhikasLegalDocument, type YhikasRoomFactsOptions, YhikasRoomPageRecordError, type YhikasRoomPagesConfig, YhikasRoomType, YhikasSiteInfo, type YhikasSyncConfig, YhikasSyncConfigError, YhikasSyncRefusedError, YhikasSyncRunError, YhikasSyncStaleError, YhikasSyncedContentReadOnlyError, YhikasUpstreamClient, type YhikasUpstreamClientOptions, YhikasUpstreamError, applyPlan, assertNotStale, assessStaleness, cadenceOf, classifyRefusal, columnOf, createRoomPageDrafts, createRoomPageRegistry, createSyncStateStore, decimalOf, isPublishedRoomType, isRoomOffer, legalDocumentRowSchema, legalDocumentsResponseSchema, machineWritten, multilingualTextSchema, planDiff, projectLegalDocuments, projectRoomTypes, projectSiteInfo, readLocalRows, recordRoomPage, resolveYhikasSyncConfig, roomFactItemsFrom, roomFactsFrom, roomFactsOf, roomPageSlug, roomTypeForPageFrom, roomTypeRowSchema, roomTypesFreshness, roomTypesResponseSchema, runYhikasSync, secondaryLocaleOf, siteInfoResponseSchema, siteNoticeRowSchema, stableHash, toRoomPageWriter, toRoomTypeReader, toSyncEntityClient, truncateError, yhikasRoomFacts, yhikasRoomPagesTable, yhikasSyncStateTable };
|
|
1503
963
|
//# sourceMappingURL=index.d.mts.map
|