@murumets-ee/yhikas-sync 0.91.0 → 0.93.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/dist/{config-CHVLiXZf.d.mts → config-ClDwmosW.d.mts} +26 -1
- package/dist/config-ClDwmosW.d.mts.map +1 -0
- package/dist/index.d.mts +117 -127
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/jobs-Vg-eMVGf.mjs +2 -0
- package/dist/jobs-Vg-eMVGf.mjs.map +1 -0
- package/dist/plugin.d.mts +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/plugin.mjs.map +1 -1
- package/dist/sync-state-table-Dq_wyKfo.mjs +2 -0
- package/dist/sync-state-table-Dq_wyKfo.mjs.map +1 -0
- package/dist/{watchdog-CcOXpzE3.mjs → watchdog-C_ClM4PH.mjs} +2 -2
- package/dist/{watchdog-CcOXpzE3.mjs.map → watchdog-C_ClM4PH.mjs.map} +1 -1
- package/package.json +7 -7
- 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
|
@@ -45,6 +45,25 @@ interface YhikasSyncConfig {
|
|
|
45
45
|
* install genuinely has no yhikas-admin to talk to.
|
|
46
46
|
*/
|
|
47
47
|
enabled?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Who draws a room's and a legal document's detail page.
|
|
50
|
+
*
|
|
51
|
+
* The default is the toolkit's: each entity's stored detail template
|
|
52
|
+
* (`drawnBy: 'template'`, block-library D009), which also narrows the public
|
|
53
|
+
* content API to what the template draws. A site that draws those pages with
|
|
54
|
+
* hand-built routes says so here, WITH a reason — the opt-out is reported at
|
|
55
|
+
* start-up (`handBuiltDetailPages`), so it is a recorded decision rather
|
|
56
|
+
* than a default (block-library PR19).
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* yhikasSync({ detailPages: { drawnBy: 'site', reason: 'Hand-built Afišš room and document pages (owner, 2026-10-02)' } })
|
|
60
|
+
*/
|
|
61
|
+
detailPages?: {
|
|
62
|
+
readonly drawnBy: 'template';
|
|
63
|
+
} | {
|
|
64
|
+
readonly drawnBy: 'site';
|
|
65
|
+
readonly reason: string;
|
|
66
|
+
};
|
|
48
67
|
}
|
|
49
68
|
interface ResolvedYhikasSyncConfig {
|
|
50
69
|
readonly baseUrl: string | undefined;
|
|
@@ -56,6 +75,12 @@ interface ResolvedYhikasSyncConfig {
|
|
|
56
75
|
readonly maxRetireFraction: number;
|
|
57
76
|
readonly minRowsForRetireFloor: number;
|
|
58
77
|
readonly enabled: boolean;
|
|
78
|
+
readonly detailPages: {
|
|
79
|
+
readonly drawnBy: 'template';
|
|
80
|
+
} | {
|
|
81
|
+
readonly drawnBy: 'site';
|
|
82
|
+
readonly reason: string;
|
|
83
|
+
};
|
|
59
84
|
}
|
|
60
85
|
/** Thrown at plugin construction for a structurally invalid config. */
|
|
61
86
|
declare class YhikasSyncConfigError extends Error {
|
|
@@ -74,4 +99,4 @@ declare class YhikasSyncConfigError extends Error {
|
|
|
74
99
|
declare function resolveYhikasSyncConfig(config?: YhikasSyncConfig, env?: NodeJS.ProcessEnv): ResolvedYhikasSyncConfig;
|
|
75
100
|
//#endregion
|
|
76
101
|
export { resolveYhikasSyncConfig as i, YhikasSyncConfig as n, YhikasSyncConfigError as r, ResolvedYhikasSyncConfig as t };
|
|
77
|
-
//# sourceMappingURL=config-
|
|
102
|
+
//# sourceMappingURL=config-ClDwmosW.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-ClDwmosW.d.mts","names":[],"sources":["../src/config.ts"],"mappings":";;AA+BA;;;;;;;;;;;;;;;;;;UAAiB,gBAAA;EA6CA;;;;EAxCf,OAAA;EA0CS;EAxCT,QAAA;EA0CS;EAxCT,gBAAA;EA0CS;EAxCT,YAAA;EA0CS;EAxCT,gBAAA;EA0CS;EAxCT,kBAAA;EA0Ce;EAxCf,iBAAA;EAwC+C;EAtC/C,qBAAA;EA0CW;;;;;;EAnCX,OAAA;EAoC2B;AAAA;AAoC7B;;;;;;;;;;;EA1DE,WAAA;IAAA,SACe,OAAA;EAAA;IAAA,SACA,OAAA;IAAA,SAA0B,MAAA;EAAA;AAAA;AAAA,UAG1B,wBAAA;EAAA,SACN,OAAA;EAAA,SACA,QAAA;EAAA,SACA,gBAAA;EAAA,SACA,YAAA;EAAA,SACA,gBAAA;EAAA,SACA,kBAAA;EAAA,SACA,iBAAA;EAAA,SACA,qBAAA;EAAA,SACA,OAAA;EAAA,SACA,WAAA;IAAA,SACM,OAAA;EAAA;IAAA,SACA,OAAA;IAAA,SAA0B,MAAA;EAAA;AAAA;;cAI9B,qBAAA,SAA8B,KAAK;cAClC,OAAA;AAAA;;;;;;;;;;;iBAoCE,uBAAA,CACd,MAAA,GAAQ,gBAAA,EACR,GAAA,GAAK,MAAA,CAAO,UAAA,GACX,wBAAA"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as resolveYhikasSyncConfig, n as YhikasSyncConfig, r as YhikasSyncConfigError, t as ResolvedYhikasSyncConfig } from "./config-
|
|
1
|
+
import { i as resolveYhikasSyncConfig, n as YhikasSyncConfig, r as YhikasSyncConfigError, t as ResolvedYhikasSyncConfig } from "./config-ClDwmosW.mjs";
|
|
2
2
|
import { Behavior } from "@murumets-ee/entity";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { ToolkitApp } from "@murumets-ee/core";
|
|
@@ -776,132 +776,6 @@ declare class YhikasSyncedContentReadOnlyError extends Error {
|
|
|
776
776
|
*/
|
|
777
777
|
declare function machineWritten<N extends string>(entityName: N): Behavior<Record<never, never>, `yhikas-sync:machine-written:${N}`>;
|
|
778
778
|
//#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
779
|
//#region src/entities/legal-document.d.ts
|
|
906
780
|
/**
|
|
907
781
|
* `yhikas_legal_document` — the local projection of yhikas-admin's
|
|
@@ -1118,6 +992,122 @@ declare const YhikasRoomType: import("@murumets-ee/entity").Entity<{
|
|
|
1118
992
|
};
|
|
1119
993
|
}, "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
994
|
//#endregion
|
|
995
|
+
//#region src/entities/index.d.ts
|
|
996
|
+
declare const SYNCED_ENTITIES: readonly [import("@murumets-ee/entity").Entity<{
|
|
997
|
+
id: import("@murumets-ee/entity").IdField;
|
|
998
|
+
} & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
|
|
999
|
+
code: import("@murumets-ee/entity").TextField & {
|
|
1000
|
+
readonly required: true;
|
|
1001
|
+
readonly unique: true;
|
|
1002
|
+
readonly indexed: true;
|
|
1003
|
+
readonly maxLength: 190;
|
|
1004
|
+
};
|
|
1005
|
+
name: import("@murumets-ee/entity").TextField & {
|
|
1006
|
+
readonly required: true;
|
|
1007
|
+
readonly translatable: true;
|
|
1008
|
+
};
|
|
1009
|
+
totalArea: import("@murumets-ee/entity").TextField & {
|
|
1010
|
+
readonly maxLength: 32;
|
|
1011
|
+
};
|
|
1012
|
+
livingArea: import("@murumets-ee/entity").TextField & {
|
|
1013
|
+
readonly maxLength: 32;
|
|
1014
|
+
};
|
|
1015
|
+
commonArea: import("@murumets-ee/entity").TextField & {
|
|
1016
|
+
readonly maxLength: 32;
|
|
1017
|
+
};
|
|
1018
|
+
capacity: import("@murumets-ee/entity").NumberField & {
|
|
1019
|
+
readonly integer: true;
|
|
1020
|
+
};
|
|
1021
|
+
placesOccupied: import("@murumets-ee/entity").NumberField & {
|
|
1022
|
+
readonly integer: true;
|
|
1023
|
+
};
|
|
1024
|
+
monthlyRent: import("@murumets-ee/entity").TextField & {
|
|
1025
|
+
readonly maxLength: 32;
|
|
1026
|
+
};
|
|
1027
|
+
discountedMonthlyRent: import("@murumets-ee/entity").TextField & {
|
|
1028
|
+
readonly maxLength: 32;
|
|
1029
|
+
};
|
|
1030
|
+
dailyRent: import("@murumets-ee/entity").TextField & {
|
|
1031
|
+
readonly maxLength: 32;
|
|
1032
|
+
};
|
|
1033
|
+
currency: import("@murumets-ee/entity").TextField & {
|
|
1034
|
+
readonly required: true;
|
|
1035
|
+
readonly maxLength: 3;
|
|
1036
|
+
readonly default: "EUR";
|
|
1037
|
+
};
|
|
1038
|
+
vatTreatment: import("@murumets-ee/entity").SelectField & {
|
|
1039
|
+
options: readonly ["net", "gross"];
|
|
1040
|
+
} & {
|
|
1041
|
+
readonly options: readonly ["net", "gross"];
|
|
1042
|
+
readonly required: true;
|
|
1043
|
+
readonly default: "net";
|
|
1044
|
+
};
|
|
1045
|
+
hasEnglish: import("@murumets-ee/entity").BooleanField & {
|
|
1046
|
+
readonly required: true;
|
|
1047
|
+
readonly default: false;
|
|
1048
|
+
};
|
|
1049
|
+
sourceHash: import("@murumets-ee/entity").TextField & {
|
|
1050
|
+
readonly maxLength: 64;
|
|
1051
|
+
};
|
|
1052
|
+
}, "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<{
|
|
1053
|
+
id: import("@murumets-ee/entity").IdField;
|
|
1054
|
+
} & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
|
|
1055
|
+
type: import("@murumets-ee/entity").TextField & {
|
|
1056
|
+
readonly required: true;
|
|
1057
|
+
readonly unique: true;
|
|
1058
|
+
readonly indexed: true;
|
|
1059
|
+
readonly maxLength: 190;
|
|
1060
|
+
};
|
|
1061
|
+
title: import("@murumets-ee/entity").TextField & {
|
|
1062
|
+
readonly required: true;
|
|
1063
|
+
readonly translatable: true;
|
|
1064
|
+
};
|
|
1065
|
+
sourceSlug: import("@murumets-ee/entity").TextField & {
|
|
1066
|
+
readonly required: true;
|
|
1067
|
+
readonly indexed: true;
|
|
1068
|
+
readonly maxLength: 190;
|
|
1069
|
+
};
|
|
1070
|
+
body: import("@murumets-ee/entity").RichTextField & {
|
|
1071
|
+
readonly required: true;
|
|
1072
|
+
readonly translatable: true;
|
|
1073
|
+
};
|
|
1074
|
+
order: import("@murumets-ee/entity").NumberField & {
|
|
1075
|
+
readonly integer: true;
|
|
1076
|
+
readonly required: true;
|
|
1077
|
+
readonly default: 0;
|
|
1078
|
+
};
|
|
1079
|
+
hasEnglish: import("@murumets-ee/entity").BooleanField & {
|
|
1080
|
+
readonly required: true;
|
|
1081
|
+
readonly default: false;
|
|
1082
|
+
};
|
|
1083
|
+
sourceHash: import("@murumets-ee/entity").TextField & {
|
|
1084
|
+
readonly maxLength: 64;
|
|
1085
|
+
};
|
|
1086
|
+
}, "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<{
|
|
1087
|
+
id: import("@murumets-ee/entity").IdField;
|
|
1088
|
+
} & import("@murumets-ee/entity").AuditableFields & import("@murumets-ee/entity").PublishableFields & Record<never, never> & {
|
|
1089
|
+
key: import("@murumets-ee/entity").TextField & {
|
|
1090
|
+
readonly required: true;
|
|
1091
|
+
readonly unique: true;
|
|
1092
|
+
readonly indexed: true;
|
|
1093
|
+
readonly maxLength: 32;
|
|
1094
|
+
};
|
|
1095
|
+
receptionHours: import("@murumets-ee/entity").TextField & {
|
|
1096
|
+
readonly translatable: true;
|
|
1097
|
+
readonly maxLength: 500;
|
|
1098
|
+
};
|
|
1099
|
+
notices: import("@murumets-ee/entity").JsonField & {
|
|
1100
|
+
readonly translatable: true;
|
|
1101
|
+
};
|
|
1102
|
+
hasEnglish: import("@murumets-ee/entity").BooleanField & {
|
|
1103
|
+
readonly default: false;
|
|
1104
|
+
readonly required: true;
|
|
1105
|
+
};
|
|
1106
|
+
sourceHash: import("@murumets-ee/entity").TextField & {
|
|
1107
|
+
readonly maxLength: 64;
|
|
1108
|
+
};
|
|
1109
|
+
}, "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">]>];
|
|
1110
|
+
//#endregion
|
|
1121
1111
|
//#region src/entities/site-info.d.ts
|
|
1122
1112
|
/**
|
|
1123
1113
|
* `yhikas_site_info` — reception hours and the notice ticker, as ONE row.
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/constants.ts","../src/diff.ts","../src/upstream/wire.ts","../src/projection.ts","../src/apply.ts","../src/entities/guard.ts","../src/entities/
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/constants.ts","../src/diff.ts","../src/upstream/wire.ts","../src/projection.ts","../src/apply.ts","../src/entities/guard.ts","../src/entities/legal-document.ts","../src/entities/room-type.ts","../src/entities/index.ts","../src/entities/site-info.ts","../src/entity-client.ts","../src/sync-state-table.ts","../src/sync-state.ts","../src/upstream/client.ts","../src/run-sync.ts","../src/upstream/errors.ts","../src/watchdog.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{C as e,D as t,E as n,O as r,S as i,T as a,_ as o,a as s,b as c,c as l,d as u,f as d,h as f,i as p,l as m,m as h,n as g,o as _,p as v,r as y,s as b,t as x,u as S,v as C,w,x as T,y as E}from"./watchdog-
|
|
1
|
+
import{C as e,D as t,E as n,O as r,S as i,T as a,_ as o,a as s,b as c,c as l,d as u,f as d,h as f,i as p,l as m,m as h,n as g,o as _,p as v,r as y,s as b,t as x,u as S,v as C,w,x as T,y as E}from"./watchdog-C_ClM4PH.mjs";import{A as D,C as O,D as k,E as A,M as j,N as M,P as N,_ as P,a as F,b as I,c as L,d as R,f as z,g as B,h as V,j as H,k as U,l as W,m as G,n as K,o as q,p as J,s as Y,t as X,u as Z,v as Q,w as $,x as ee,y as te}from"./sync-state-table-Dq_wyKfo.mjs";export{B as API_KEY_ENV_VAR,P as BASE_URL_ENV_VAR,Q as DECLARED_CURRENCY,te as DECLARED_VAT_TREATMENT,I as EN_LOCALE,ee as ET_LOCALE,O as LEGAL_DOCUMENTS_RESOURCE,$ as MAX_SITE_NOTICES,A as ROOM_TYPES_RESOURCE,k as SITE_INFO_KEY,U as SITE_INFO_RESOURCE,K as SYNCED_ENTITIES,D as SYNC_JOB_NAME,H as SYNC_RESOURCES,Y as VAT_TREATMENTS,j as WATCHDOG_JOB_NAME,Z as YHIKAS_LEGAL_DOCUMENT_ENTITY,L as YHIKAS_ROOM_TYPE_ENTITY,F as YHIKAS_SITE_INFO_ENTITY,M as YHIKAS_SYNC_ACTOR_ID,N as YHIKAS_SYNC_PLUGIN_NAME,n as YhikasApplyPartialError,R as YhikasLegalDocument,W as YhikasRoomType,q as YhikasSiteInfo,G as YhikasSyncConfigError,w as YhikasSyncRefusedError,h as YhikasSyncRunError,x as YhikasSyncStaleError,z as YhikasSyncedContentReadOnlyError,p as YhikasUpstreamClient,a as YhikasUpstreamError,t as applyPlan,g as assertNotStale,y as assessStaleness,d as createSyncStateStore,s as legalDocumentRowSchema,_ as legalDocumentsResponseSchema,J as machineWritten,b as multilingualTextSchema,i as planDiff,o as projectLegalDocuments,C as projectRoomTypes,E as projectSiteInfo,r as readLocalRows,V as resolveYhikasSyncConfig,l as roomTypeRowSchema,m as roomTypesResponseSchema,f as runYhikasSync,c as secondaryLocaleOf,S as siteInfoResponseSchema,u as siteNoticeRowSchema,e as stableHash,T as toSyncEntityClient,v as truncateError,X as yhikasSyncStateTable};
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{b as e,f as t,g as n,h as r,i,n as a,x as o}from"./watchdog-C_ClM4PH.mjs";import{N as s,_ as c,g as l,j as u,o as d,r as f,t as p}from"./sync-state-table-Dq_wyKfo.mjs";import{elevateRequestContext as m,runWithContextAsync as h}from"@murumets-ee/core";var g=class extends Error{constructor(e){super(`yhikas-sync is enabled but ${e.join(` and `)} ${e.length===1?`is`:`are`} not set. Failing loudly rather than skipping quietly: an unconfigured sync is a broken sync, and a broken sync must be visibly broken. Set the variable, or pass \`enabled: false\` to yhikasSync() if this install has no yhikas-admin.`),this.name=`YhikasSyncNotConfiguredError`}};function _(){return m({user:{id:s,groups:[`admin`]}})}function v(e){return e.logger}async function y(t){let{resolveI18nConfig:r}=await import(`@murumets-ee/content`),{defaultLocale:i}=await r(t);if(!n.includes(i))throw new b(`The app's default locale is '${i}', but yhikas-admin carries content in 'et' and 'en' only. The sync has nothing to write to the base row, so it refuses rather than publishing one language under another's name.`);return{defaultLocale:i,secondaryLocale:e(i)}}var b=class extends Error{constructor(e){super(e),this.name=`YhikasSyncLocaleError`}};function x(e,t){let n=t[l],r=[];if(e.baseUrl||r.push(c),n||r.push(l),!e.baseUrl||!n)throw new g(r);return{baseUrl:e.baseUrl,apiKey:n}}async function S(e,n,a=process.env){let{baseUrl:s,apiKey:c}=x(n,a),l=f(n.detailPages),{sanitizeHtml:u}=await import(`@murumets-ee/blocks`);return h(_(),async()=>{let{defaultLocale:a}=await y(e);return r({upstream:new i({baseUrl:s,apiKey:c,timeoutMs:n.requestTimeoutMs}),roomTypeClient:o(e.getClient(l.roomType),a),legalDocumentClient:o(e.getClient(l.legalDocument),a),siteInfoClient:o(e.getClient(d),a),state:t(p.makeClient(e.db.readWrite)),sanitizeHtml:u,logger:v(e),defaultLocale:a,floor:{maxRows:n.maxRowsPerResource,maxRetireFraction:n.maxRetireFraction,minRowsForFraction:n.minRowsForRetireFloor}})})}async function C(e,n,r=new Date){let i=t(p.makeClient(e.db.readWrite));await w(e,r);let o=a(await i.readAll(),r,n.staleAfterMs);return e.logger.debug({window:n.staleAfterMs,resources:o.map(e=>({resource:e.resource,lastSuccessAt:e.lastSuccessAt}))},`yhikas-sync: staleness check passed`),o}async function w(e,n=new Date){let r=t(p.makeClient(e.db.readWrite));for(let e of u)await r.ensure(e,n)}export{S as runSyncJob,C as runWatchdogJob};
|
|
2
|
+
//# sourceMappingURL=jobs-Vg-eMVGf.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs-Vg-eMVGf.mjs","names":[],"sources":["../src/jobs.ts"],"sourcesContent":["/**\n * The two job handlers, bound to a live `ToolkitApp`.\n *\n * This module is reached ONLY through a dynamic `import()` from the plugin's\n * `init` (see `plugin.ts`). It statically imports `@murumets-ee/core` and\n * `@murumets-ee/blocks`, either of which would be evaluated at\n * `lumi.config.ts`-load time under the CLI's jiti loader if the plugin entry\n * reached it statically — and `@murumets-ee/blocks`' root export evaluates\n * React's `createContext` at module scope.\n */\n\nimport { elevateRequestContext, runWithContextAsync, type ToolkitApp } from '@murumets-ee/core'\nimport type { SyncLogger } from './apply.js'\nimport type { ResolvedYhikasSyncConfig } from './config.js'\nimport {\n API_KEY_ENV_VAR,\n BASE_URL_ENV_VAR,\n EN_LOCALE,\n ET_LOCALE,\n LEGAL_DOCUMENTS_RESOURCE,\n ROOM_TYPES_RESOURCE,\n SYNC_RESOURCES,\n YHIKAS_SYNC_ACTOR_ID,\n} from './constants.js'\nimport { detailPageEntities } from './entities/index.js'\nimport { YhikasSiteInfo } from './entities/site-info.js'\nimport { toSyncEntityClient } from './entity-client.js'\nimport { SUPPORTED_LOCALES, secondaryLocaleOf } from './projection.js'\nimport { runYhikasSync, type SyncRunSummary } from './run-sync.js'\nimport { createSyncStateStore } from './sync-state.js'\nimport { yhikasSyncStateTable } from './sync-state-table.js'\nimport { YhikasUpstreamClient } from './upstream/client.js'\nimport { assertNotStale, type ResourceStaleness } from './watchdog.js'\n\n/** Thrown when the sync is enabled but has no credential or no origin to talk to. */\nexport class YhikasSyncNotConfiguredError extends Error {\n constructor(missing: readonly string[]) {\n super(\n `yhikas-sync is enabled but ${missing.join(' and ')} ${missing.length === 1 ? 'is' : 'are'} ` +\n `not set. Failing loudly rather than skipping quietly: an unconfigured sync is a broken ` +\n `sync, and a broken sync must be visibly broken. Set the variable, or pass ` +\n `\\`enabled: false\\` to yhikasSync() if this install has no yhikas-admin.`,\n )\n this.name = 'YhikasSyncNotConfiguredError'\n }\n}\n\n/**\n * Every entity write runs as this identity.\n *\n * `elevateRequestContext` rather than `runAsCli`: both replace the permission\n * checker, but this one carries a NAMED actor, so `auditable()` records\n * `yhikas-sync` in `created_by`/`updated_by` instead of a generic `cli`. That\n * makes the sync's writes attributable — worth having precisely because the\n * upstream writes are not (F006) — and it is what the machine-written guard\n * recognises when it refuses every other writer.\n *\n * Built per run rather than at module scope: `elevateRequestContext` reads the\n * ambient context at CALL time, and a module-scope constant would bake in\n * whatever context happened to exist when the module first loaded.\n */\nfunction syncContext() {\n return elevateRequestContext({\n user: { id: YHIKAS_SYNC_ACTOR_ID, groups: ['admin'] },\n })\n}\n\n/** Narrow `app.logger` to the shape the sync uses, without importing a logger type. */\nfunction toSyncLogger(app: ToolkitApp): SyncLogger {\n return app.logger\n}\n\n/**\n * The app's REAL default locale, read from the content plugin rather than\n * configured here.\n *\n * A configured copy could disagree with the app's actual default, and the\n * failure would be silent in both directions: `updateForLocale` would take its\n * `isDefault` branch for what this package thought was the secondary locale\n * and overwrite the base row with the wrong language, while no translation was\n * ever written for the real secondary. `resolveI18nConfig` is the authoritative\n * answer — it merges the runtime `i18n` override over the shipped config — so\n * there is nothing to keep in sync.\n */\nasync function resolveLocales(\n app: ToolkitApp,\n): Promise<{ defaultLocale: string; secondaryLocale: string }> {\n const { resolveI18nConfig } = await import('@murumets-ee/content')\n const { defaultLocale } = await resolveI18nConfig(app)\n\n if (!SUPPORTED_LOCALES.includes(defaultLocale)) {\n throw new YhikasSyncLocaleError(\n `The app's default locale is '${defaultLocale}', but yhikas-admin carries content in ` +\n `'${ET_LOCALE}' and '${EN_LOCALE}' only. The sync has nothing to write to the base row, ` +\n `so it refuses rather than publishing one language under another's name.`,\n )\n }\n // `secondaryLocaleOf` asserts the same thing, so the two can never disagree.\n return { defaultLocale, secondaryLocale: secondaryLocaleOf(defaultLocale) }\n}\n\n/** Thrown when the app's locale configuration cannot carry the synced content. */\nexport class YhikasSyncLocaleError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'YhikasSyncLocaleError'\n }\n}\n\n/**\n * Resolve the bearer credential at RUN time, from the environment.\n *\n * Deliberately not part of the plugin's declared config: resolved config lands\n * in the framework's `PluginConfigRegistry`, a process-global any plugin can\n * read, and a secret does not belong there (mirroring `@murumets-ee/merit`'s\n * D003). Reading it here also means rotating the key needs a restart rather\n * than a rebuild.\n */\nfunction resolveCredentials(\n config: ResolvedYhikasSyncConfig,\n env: NodeJS.ProcessEnv,\n): { baseUrl: string; apiKey: string } {\n const apiKey = env[API_KEY_ENV_VAR]\n const missing: string[] = []\n if (!config.baseUrl) missing.push(BASE_URL_ENV_VAR)\n if (!apiKey) missing.push(API_KEY_ENV_VAR)\n if (!config.baseUrl || !apiKey) throw new YhikasSyncNotConfiguredError(missing)\n return { baseUrl: config.baseUrl, apiKey }\n}\n\n/** One full sync run. Throws on any resource failure, so the queue retries and alerts. */\nexport async function runSyncJob(\n app: ToolkitApp,\n config: ResolvedYhikasSyncConfig,\n env: NodeJS.ProcessEnv = process.env,\n): Promise<SyncRunSummary> {\n const { baseUrl, apiKey } = resolveCredentials(config, env)\n const detailEntities = detailPageEntities(config.detailPages)\n const { sanitizeHtml } = await import('@murumets-ee/blocks')\n\n return runWithContextAsync(syncContext(), async () => {\n const { defaultLocale } = await resolveLocales(app)\n return runYhikasSync({\n upstream: new YhikasUpstreamClient({\n baseUrl,\n apiKey,\n timeoutMs: config.requestTimeoutMs,\n }),\n // The clients are built HERE, inside the job, not in the plugin's\n // `init`. That is load-bearing and easy to \"tidy\" into a bug:\n // `createAdminClient` reads the locale-status writer ONCE at\n // construction, and `@murumets-ee/content` installs that writer from its\n // own `init`. `Plugin.requires` guarantees content is CONFIGURED, not\n // that it initialised first — so a client constructed during init could\n // capture no writer, and `updateForLocale` would then silently assign\n // the requested status to the BASE row. Constructing at job-run time is\n // after every plugin's init, unconditionally.\n // The definitions the plugin REGISTERED — the hand-built variants when\n // the site chose them (`detailPages`) — so the sync writes through the\n // entity the app actually serves.\n roomTypeClient: toSyncEntityClient(app.getClient(detailEntities.roomType), defaultLocale),\n legalDocumentClient: toSyncEntityClient(\n app.getClient(detailEntities.legalDocument),\n defaultLocale,\n ),\n siteInfoClient: toSyncEntityClient(app.getClient(YhikasSiteInfo), defaultLocale),\n state: createSyncStateStore(yhikasSyncStateTable.makeClient(app.db.readWrite)),\n sanitizeHtml,\n logger: toSyncLogger(app),\n defaultLocale,\n floor: {\n maxRows: config.maxRowsPerResource,\n maxRetireFraction: config.maxRetireFraction,\n minRowsForFraction: config.minRowsForRetireFloor,\n },\n })\n })\n}\n\n/**\n * The staleness check. Throws when any resource is past the window, which is\n * how it reaches the queue's dead-letter alerting — the detection is new, the\n * delivery is not.\n */\nexport async function runWatchdogJob(\n app: ToolkitApp,\n config: ResolvedYhikasSyncConfig,\n now: Date = new Date(),\n): Promise<ResourceStaleness[]> {\n const store = createSyncStateStore(yhikasSyncStateTable.makeClient(app.db.readWrite))\n // Seed before assessing, so a fresh install gets a `firstSeenAt` to be\n // measured from rather than reading as \"no tracking row at all\" — which is\n // correct but alarming on an app that simply has not reached its first sync\n // tick yet.\n //\n // The cost, stated: `assessStaleness`'s missing-row branch is therefore\n // unreachable from here, so a tracking row DELETED by hand while the sync is\n // broken resets the clock instead of alerting. That needs deliberate DB\n // surgery to reach, and the alternative — alerting on every fresh install's\n // first tick — trains people to ignore the one channel that has to stay\n // trustworthy.\n await ensureSyncState(app, now)\n const records = await store.readAll()\n const assessed = assertNotStale(records, now, config.staleAfterMs)\n app.logger.debug(\n {\n window: config.staleAfterMs,\n resources: assessed.map((entry) => ({\n resource: entry.resource,\n lastSuccessAt: entry.lastSuccessAt,\n })),\n },\n 'yhikas-sync: staleness check passed',\n )\n return assessed\n}\n\n/**\n * Seed a tracking row per resource, so a fresh install has a clock to be\n * measured against before its first sync tick.\n *\n * Called from the WATCHDOG, not from the plugin's `init`. Doing DB I/O during\n * `init` runs it in every process that builds a `ToolkitApp` — web included —\n * and would fail app boot outright on a database blip, or on any boot that\n * precedes `lumi migrate` creating the table, in exchange for two bookkeeping\n * rows.\n */\nexport async function ensureSyncState(app: ToolkitApp, now: Date = new Date()): Promise<void> {\n const store = createSyncStateStore(yhikasSyncStateTable.makeClient(app.db.readWrite))\n for (const resource of SYNC_RESOURCES) {\n await store.ensure(resource, now)\n }\n}\n\nexport { LEGAL_DOCUMENTS_RESOURCE, ROOM_TYPES_RESOURCE }\n"],"mappings":"kQAmCA,IAAa,EAAb,cAAkD,KAAM,CACtD,YAAY,EAA4B,CACtC,MACE,8BAA8B,EAAQ,KAAK,OAAO,EAAE,GAAG,EAAQ,SAAW,EAAI,KAAO,MAAM,0OAI7F,EACA,KAAK,KAAO,8BACd,CACF,EAgBA,SAAS,GAAc,CACrB,OAAO,EAAsB,CAC3B,KAAM,CAAE,GAAI,EAAsB,OAAQ,CAAC,OAAO,CAAE,CACtD,CAAC,CACH,CAGA,SAAS,EAAa,EAA6B,CACjD,OAAO,EAAI,MACb,CAcA,eAAe,EACb,EAC6D,CAC7D,GAAM,CAAE,qBAAsB,MAAM,OAAO,wBACrC,CAAE,iBAAkB,MAAM,EAAkB,CAAG,EAErD,GAAI,CAAC,EAAkB,SAAS,CAAa,EAC3C,MAAM,IAAI,EACR,gCAAgC,EAAc,kLAGhD,EAGF,MAAO,CAAE,gBAAe,gBAAiB,EAAkB,CAAa,CAAE,CAC5E,CAGA,IAAa,EAAb,cAA2C,KAAM,CAC/C,YAAY,EAAiB,CAC3B,MAAM,CAAO,EACb,KAAK,KAAO,uBACd,CACF,EAWA,SAAS,EACP,EACA,EACqC,CACrC,IAAM,EAAS,EAAI,GACb,EAAoB,CAAC,EAG3B,GAFK,EAAO,SAAS,EAAQ,KAAK,CAAgB,EAC7C,GAAQ,EAAQ,KAAK,CAAe,EACrC,CAAC,EAAO,SAAW,CAAC,EAAQ,MAAM,IAAI,EAA6B,CAAO,EAC9E,MAAO,CAAE,QAAS,EAAO,QAAS,QAAO,CAC3C,CAGA,eAAsB,EACpB,EACA,EACA,EAAyB,QAAQ,IACR,CACzB,GAAM,CAAE,UAAS,UAAW,EAAmB,EAAQ,CAAG,EACpD,EAAiB,EAAmB,EAAO,WAAW,EACtD,CAAE,gBAAiB,MAAM,OAAO,uBAEtC,OAAO,EAAoB,EAAY,EAAG,SAAY,CACpD,GAAM,CAAE,iBAAkB,MAAM,EAAe,CAAG,EAClD,OAAO,EAAc,CACnB,SAAU,IAAI,EAAqB,CACjC,UACA,SACA,UAAW,EAAO,gBACpB,CAAC,EAaD,eAAgB,EAAmB,EAAI,UAAU,EAAe,QAAQ,EAAG,CAAa,EACxF,oBAAqB,EACnB,EAAI,UAAU,EAAe,aAAa,EAC1C,CACF,EACA,eAAgB,EAAmB,EAAI,UAAU,CAAc,EAAG,CAAa,EAC/E,MAAO,EAAqB,EAAqB,WAAW,EAAI,GAAG,SAAS,CAAC,EAC7E,eACA,OAAQ,EAAa,CAAG,EACxB,gBACA,MAAO,CACL,QAAS,EAAO,mBAChB,kBAAmB,EAAO,kBAC1B,mBAAoB,EAAO,qBAC7B,CACF,CAAC,CACH,CAAC,CACH,CAOA,eAAsB,EACpB,EACA,EACA,EAAY,IAAI,KACc,CAC9B,IAAM,EAAQ,EAAqB,EAAqB,WAAW,EAAI,GAAG,SAAS,CAAC,EAYpF,MAAM,EAAgB,EAAK,CAAG,EAE9B,IAAM,EAAW,EAAe,MADV,EAAM,QAAQ,EACK,EAAK,EAAO,YAAY,EAWjE,OAVA,EAAI,OAAO,MACT,CACE,OAAQ,EAAO,aACf,UAAW,EAAS,IAAK,IAAW,CAClC,SAAU,EAAM,SAChB,cAAe,EAAM,aACvB,EAAE,CACJ,EACA,qCACF,EACO,CACT,CAYA,eAAsB,EAAgB,EAAiB,EAAY,IAAI,KAAuB,CAC5F,IAAM,EAAQ,EAAqB,EAAqB,WAAW,EAAI,GAAG,SAAS,CAAC,EACpF,IAAK,IAAM,KAAY,EACrB,MAAM,EAAM,OAAO,EAAU,CAAG,CAEpC"}
|
package/dist/plugin.d.mts
CHANGED
package/dist/plugin.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{A as e,M as t,
|
|
1
|
+
import{A as e,M as t,P as n,h as r,i,t as a}from"./sync-state-table-Dq_wyKfo.mjs";import{definePlugin as o}from"@murumets-ee/core";function s(s){let c=r(s);return o({name:n,requires:[`@murumets-ee/content`,`@murumets-ee/queue`],server:{config:c,entities:[...i(c.detailPages)],tables:{yhikas_sync_state:a.table},init:async r=>{if(!c.enabled){r.logger.warn({plugin:n},`yhikas-sync is disabled by config — no scheduled pull, and no staleness alerting`);return}let{defineJob:i,registerJob:a}=await import(`@murumets-ee/queue/client`);a(i({name:e,description:`Pull room types and legal documents from yhikas-admin and apply a full-snapshot diff.`,schedule:c.schedule}),async()=>{let{runSyncJob:e}=await import(`./jobs-Vg-eMVGf.mjs`);await e(r,c)}),a(i({name:t,description:`Alert when the newest successful yhikas-admin sync is older than the staleness window.`,schedule:c.watchdogSchedule}),async()=>{let{runWatchdogJob:e}=await import(`./jobs-Vg-eMVGf.mjs`);await e(r,c)})}}})}export{s as yhikasSync};
|
|
2
2
|
//# sourceMappingURL=plugin.mjs.map
|
package/dist/plugin.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * `@murumets-ee/yhikas-sync/plugin` — the jiti-safe plugin entry.\n *\n * Apps wire the sync by importing `yhikasSync()` into their `lumi.config.ts`,\n * which the `lumi` CLI loads through jiti. A static `import 'server-only'`\n * anywhere in THIS module's top-level graph throws under plain Node and breaks\n * every `lumi` command (see `gotcha_server_only_in_cli_load_path`), so the\n * static imports here are limited to pure schema and config modules. The job\n * bodies — which reach `@murumets-ee/core`, `@murumets-ee/blocks` and the\n * entity admin client — are behind a dynamic `import()` that only runs when a\n * worker actually invokes a handler.\n */\n\nimport { definePlugin, type ToolkitApp } from '@murumets-ee/core'\nimport {\n type ResolvedYhikasSyncConfig,\n resolveYhikasSyncConfig,\n type YhikasSyncConfig,\n} from './config.js'\nimport { SYNC_JOB_NAME, WATCHDOG_JOB_NAME, YHIKAS_SYNC_PLUGIN_NAME } from './constants.js'\nimport {
|
|
1
|
+
{"version":3,"file":"plugin.mjs","names":[],"sources":["../src/plugin.ts"],"sourcesContent":["/**\n * `@murumets-ee/yhikas-sync/plugin` — the jiti-safe plugin entry.\n *\n * Apps wire the sync by importing `yhikasSync()` into their `lumi.config.ts`,\n * which the `lumi` CLI loads through jiti. A static `import 'server-only'`\n * anywhere in THIS module's top-level graph throws under plain Node and breaks\n * every `lumi` command (see `gotcha_server_only_in_cli_load_path`), so the\n * static imports here are limited to pure schema and config modules. The job\n * bodies — which reach `@murumets-ee/core`, `@murumets-ee/blocks` and the\n * entity admin client — are behind a dynamic `import()` that only runs when a\n * worker actually invokes a handler.\n */\n\nimport { definePlugin, type ToolkitApp } from '@murumets-ee/core'\nimport {\n type ResolvedYhikasSyncConfig,\n resolveYhikasSyncConfig,\n type YhikasSyncConfig,\n} from './config.js'\nimport { SYNC_JOB_NAME, WATCHDOG_JOB_NAME, YHIKAS_SYNC_PLUGIN_NAME } from './constants.js'\nimport { syncedEntities } from './entities/index.js'\nimport { yhikasSyncStateTable } from './sync-state-table.js'\n\ndeclare module '@murumets-ee/core' {\n interface PluginConfigRegistry {\n '@murumets-ee/yhikas-sync': ResolvedYhikasSyncConfig\n }\n}\n\n/**\n * The yhikas-admin sync plugin.\n *\n * @example\n * ```ts\n * // lumi.config.ts\n * import { yhikasSync } from '@murumets-ee/yhikas-sync/plugin'\n * export default defineLumiConfig({ plugins: [content({...}), queue(), yhikasSync()] })\n * ```\n */\nexport function yhikasSync(config?: YhikasSyncConfig) {\n const resolved = resolveYhikasSyncConfig(config)\n\n return definePlugin({\n name: YHIKAS_SYNC_PLUGIN_NAME,\n\n /**\n * `@murumets-ee/content` is a hard requirement because without it there is\n * no locale-status writer: `createAdminClient` finds none, and\n * `updateForLocale`'s fallback assigns the requested status to the BASE\n * row — so unpublishing the English locale would silently unpublish the\n * Estonian one.\n *\n * **What `requires` does and does not buy.** The framework validates it at\n * the start of `createApp`, before any init runs, but the check is\n * PRESENCE-ONLY — its own throw message says \"plugin-array order does not\n * matter\". It cannot guarantee that content's `init` has already installed\n * the writer.\n *\n * The ordering half is bought elsewhere, and it is the easier one to break\n * by accident: `readActiveLocaleStatusWriter` runs once at AdminClient\n * CONSTRUCTION, and this package constructs its clients inside the job\n * handler (see `jobs.ts#runSyncJob`), which is after every plugin's init\n * unconditionally. Hoisting that construction into `init` — a natural-\n * looking tidy-up, since the queue wiring already lives here — would\n * reintroduce the exact hazard while leaving this declaration green.\n *\n * `@murumets-ee/queue` is required for the obvious reason: without it there\n * is no scheduler and the sync would never run.\n */\n requires: ['@murumets-ee/content', '@murumets-ee/queue'],\n\n server: {\n config: resolved,\n // The site's choice of who draws the detail pages — see `detailPages`.\n entities: [...syncedEntities(resolved.detailPages)],\n // The Drizzle table, not the `defineTable` wrapper: the framework's merge\n // step calls `schemaRegistry.register(getTableName(table), table)` on\n // each entry. Declaring it here (rather than registering from `init`)\n // means it is registered BEFORE any init runs — which is what `lumi\n // migrate` needs in order to see it in the runtime schema diff.\n tables: { yhikas_sync_state: yhikasSyncStateTable.table },\n\n /**\n * `init` is the ONE sanctioned imperative slot — queue handler\n * registration (CLAUDE.md's carve-out). There is no `Plugin.server.jobs`\n * field to declare into; every consumer in this repo does exactly this.\n */\n init: async (app: ToolkitApp) => {\n if (!resolved.enabled) {\n app.logger.warn(\n { plugin: YHIKAS_SYNC_PLUGIN_NAME },\n 'yhikas-sync is disabled by config — no scheduled pull, and no staleness alerting',\n )\n return\n }\n\n const { defineJob, registerJob } = await import('@murumets-ee/queue/client')\n\n const syncJob = defineJob({\n name: SYNC_JOB_NAME,\n description:\n 'Pull room types and legal documents from yhikas-admin and apply a full-snapshot diff.',\n schedule: resolved.schedule,\n })\n\n registerJob(syncJob, async () => {\n const { runSyncJob } = await import('./jobs.js')\n // Rethrown deliberately, unlike `toolkit:trash-purge`'s swallow: a\n // failed sync must count against `maxRetries` and reach the\n // dead-letter alerter. A partial sweep of trashed rows self-heals\n // next tick; a site serving last month's prices does not announce\n // itself.\n await runSyncJob(app, resolved)\n })\n\n const watchdogJob = defineJob({\n name: WATCHDOG_JOB_NAME,\n description:\n 'Alert when the newest successful yhikas-admin sync is older than the staleness window.',\n schedule: resolved.watchdogSchedule,\n })\n\n registerJob(watchdogJob, async () => {\n const { runWatchdogJob } = await import('./jobs.js')\n // Throwing IS the alert: the queue's alerter fires only from\n // `failJob`'s dead-letter branch, so this is how a check that\n // detects ABSENCE of success reaches a channel that only knows how\n // to report presence of an error.\n await runWatchdogJob(app, resolved)\n })\n },\n },\n })\n}\n\nexport type { ResolvedYhikasSyncConfig, YhikasSyncConfig }\n"],"mappings":"mIAuCA,SAAgB,EAAW,EAA2B,CACpD,IAAM,EAAW,EAAwB,CAAM,EAE/C,OAAO,EAAa,CAClB,KAAM,EA0BN,SAAU,CAAC,uBAAwB,oBAAoB,EAEvD,OAAQ,CACN,OAAQ,EAER,SAAU,CAAC,GAAG,EAAe,EAAS,WAAW,CAAC,EAMlD,OAAQ,CAAE,kBAAmB,EAAqB,KAAM,EAOxD,KAAM,KAAO,IAAoB,CAC/B,GAAI,CAAC,EAAS,QAAS,CACrB,EAAI,OAAO,KACT,CAAE,OAAQ,CAAwB,EAClC,kFACF,EACA,MACF,CAEA,GAAM,CAAE,YAAW,eAAgB,MAAM,OAAO,6BAShD,EAPgB,EAAU,CACxB,KAAM,EACN,YACE,wFACF,SAAU,EAAS,QACrB,CAEkB,EAAG,SAAY,CAC/B,GAAM,CAAE,cAAe,MAAM,OAAO,uBAMpC,MAAM,EAAW,EAAK,CAAQ,CAChC,CAAC,EASD,EAPoB,EAAU,CAC5B,KAAM,EACN,YACE,yFACF,SAAU,EAAS,gBACrB,CAEsB,EAAG,SAAY,CACnC,GAAM,CAAE,kBAAmB,MAAM,OAAO,uBAKxC,MAAM,EAAe,EAAK,CAAQ,CACpC,CAAC,CACH,CACF,CACF,CAAC,CACH"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{auditable as e,defineEntity as t,field as n,publishable as r}from"@murumets-ee/entity";import{column as i,defineTable as a}from"@murumets-ee/db";const o=`@murumets-ee/yhikas-sync`,s=`yhikas-sync`,c=`yhikas-sync:pull`,l=`yhikas-sync:staleness-watchdog`,u=`room_types`,d=`legal_documents`,f=`site_info`,p=[u,d,f],m=`default`,h=`et`,g=`en`,_=`EUR`,v=`net`,y=`/api/public/room-types`,b=`/api/public/legal-documents`,x=`/api/public/site-info`,S=25,C=`YHIKAS_SYNC_API_KEY`,w=`YHIKAS_ADMIN_BASE_URL`;var T=class extends Error{constructor(e){super(e),this.name=`YhikasSyncConfigError`}};function E(e,t){if(!Number.isSafeInteger(e)||e<=0)throw new T(`${t} must be a positive integer, got ${e}`);return e}function D(e,t){if(!Number.isFinite(e)||e<=0)throw new T(`${t} must be a finite positive number, got ${e}`);return e}function O(e={},t=process.env){let n=e.maxRetireFraction??.5;if(!Number.isFinite(n)||n<=0||n>1)throw new T(`maxRetireFraction must be in (0, 1], got ${n}. A value of 1 disables the fraction rule; the empty-snapshot rule still applies.`);let r=e.baseUrl??t.YHIKAS_ADMIN_BASE_URL;return Object.freeze({baseUrl:r&&r.length>0?r:void 0,schedule:e.schedule??`@every 6h`,watchdogSchedule:e.watchdogSchedule??`@every 1h`,staleAfterMs:D(e.staleAfterMs??864e5,`staleAfterMs`),requestTimeoutMs:D(e.requestTimeoutMs??15e3,`requestTimeoutMs`),maxRowsPerResource:E(e.maxRowsPerResource??500,`maxRowsPerResource`),maxRetireFraction:n,minRowsForRetireFloor:E(e.minRowsForRetireFloor??3,`minRowsForRetireFloor`),enabled:e.enabled??!0,detailPages:k(e.detailPages)})}function k(e){if(e===void 0||e.drawnBy===`template`)return Object.freeze({drawnBy:`template`});if(e.drawnBy!==`site`)throw new T(`detailPages.drawnBy must be 'template' or 'site', got ${String(e.drawnBy)}.`);let t=typeof e.reason==`string`?e.reason.trim():``;if(t===``)throw new T("detailPages: { drawnBy: 'site' } needs a `reason` — why this site draws room and document pages by hand instead of with their detail templates.");return Object.freeze({drawnBy:`site`,reason:t})}var A=class extends Error{constructor(e,t,n){super(`'${e}' is written only by the yhikas-admin sync. Refusing ${t} by '${n}': ${t===`translation write`||t===`locale publish change`?`a change here would NOT be reverted on the next sync run — it does not alter sourceHash, so the differ reports the row unchanged and the divergence from yhikas-admin stands permanently, with no alert and no staleness`:`an edit here would be silently reverted on the next sync run, which is worse than no editor at all`}. Change the value in yhikas-admin instead.`),this.name=`YhikasSyncedContentReadOnlyError`}};function j(e,t,n){let r=n?.user?.id;if(r!==`yhikas-sync`)throw new A(e,t,r??`<unattributed>`)}function M(e){return{name:`yhikas-sync:machine-written:${e}`,hooks:{beforeCreate:async(t,n)=>(j(e,`create`,n),t),beforeUpdate:async(t,n,r)=>(j(e,`update`,r),n),beforeDelete:async(t,n)=>{j(e,`delete`,n)}},assertBulkUpdate:()=>{throw new A(e,`bulk update`,`<bulk>`)},assertTranslationWrite:(t,n)=>{j(e,t.action===`locale-status`?`locale publish change`:`translation write`,n)}}}const N=`yhikas_legal_document`,P=t({name:N,scope:`global`,frontend:{path:{et:`/dokumendid/[sourceSlug]`,en:`/documents/[sourceSlug]`},drawnBy:`template`},writtenBy:`machine`,fields:{type:n.text({required:!0,unique:!0,indexed:!0,maxLength:190}),title:n.text({required:!0,translatable:!0}),sourceSlug:n.text({required:!0,indexed:!0,maxLength:190}),body:n.richtext({required:!0,translatable:!0}),order:n.number({integer:!0,required:!0,default:0}),hasEnglish:n.boolean({required:!0,default:!1}),sourceHash:n.text({maxLength:64})},behaviors:[e(),r(),M(N)],access:{view:`group.admin`,create:`group.admin`,update:`group.admin`,delete:`group.admin`},admin:{group:`yhikas`,label:`Legal documents`,labelSingular:`Legal document`,titleField:`type`,icon:`file-text`,description:`Synced from yhikas-admin every 6 hours. Read-only here — an edit would be reverted on the next run. Change the text in yhikas-admin.`,defaultSort:`order`,defaultSortDirection:`asc`,disableCreate:!0,hideFromDashboard:!0,hiddenFields:[`sourceHash`],hiddenColumns:[`sourceHash`]}}),F=`yhikas_room_type`,I=[`net`,`gross`],L=t({name:F,scope:`global`,frontend:{path:{et:`/toad/[code]`,en:`/rooms/[code]`},drawnBy:`template`},writtenBy:`machine`,fields:{code:n.text({required:!0,unique:!0,indexed:!0,maxLength:190}),name:n.text({required:!0,translatable:!0}),totalArea:n.text({maxLength:32}),livingArea:n.text({maxLength:32}),commonArea:n.text({maxLength:32}),capacity:n.number({integer:!0}),placesOccupied:n.number({integer:!0}),monthlyRent:n.text({maxLength:32}),discountedMonthlyRent:n.text({maxLength:32}),dailyRent:n.text({maxLength:32}),currency:n.text({required:!0,maxLength:3,default:`EUR`}),vatTreatment:n.select({options:I,required:!0,default:`net`}),hasEnglish:n.boolean({required:!0,default:!1}),sourceHash:n.text({maxLength:64})},behaviors:[e(),r(),M(F)],access:{view:`group.admin`,create:`group.admin`,update:`group.admin`,delete:`group.admin`},admin:{group:`yhikas`,label:`Room types`,labelSingular:`Room type`,titleField:`code`,icon:`bed-double`,description:`Synced from yhikas-admin every 6 hours. Read-only here — an edit would be reverted on the next run. Change prices in yhikas-admin.`,defaultSort:`code`,defaultSortDirection:`asc`,disableCreate:!0,hideFromDashboard:!0,hiddenFields:[`sourceHash`],hiddenColumns:[`sourceHash`]}}),R=`yhikas_site_info`,z=t({name:R,kind:`singleton`,scope:`global`,fields:{key:n.text({required:!0,unique:!0,indexed:!0,maxLength:32}),receptionHours:n.text({translatable:!0,maxLength:500}),notices:n.json({translatable:!0}),hasEnglish:n.boolean({default:!1,required:!0}),sourceHash:n.text({maxLength:64})},behaviors:[e(),r(),M(R)],access:{view:`group.admin`,create:`group.admin`,update:`group.admin`,delete:`group.admin`},admin:{group:`yhikas`,label:`Site info`,titleField:`key`,disableCreate:!0,hideFromDashboard:!0,hiddenFields:[`sourceHash`],hiddenColumns:[`sourceHash`]}}),B=[L,P,z];function V(e){if(e.drawnBy===`template`)return{roomType:L,legalDocument:P};let t=t=>{let n=t.frontend?.path;if(n===void 0)throw Error(`yhikas-sync: ${t.name} declares no frontend path to draw by hand`);return{...t,frontend:{path:n,drawnBy:`site`,reason:e.reason}}};return{roomType:t(L),legalDocument:t(P)}}function H(e){if(e.drawnBy===`template`)return B;let{roomType:t,legalDocument:n}=V(e);return[t,n,z]}const U=a({name:`yhikas_sync_state`,columns:{resource:i.varchar({length:64,notNull:!0}),firstSeenAt:i.timestamp({notNull:!0,withTimezone:!0,defaultNow:!0,pgName:`first_seen_at`}),lastAttemptAt:i.timestamp({withTimezone:!0,pgName:`last_attempt_at`}),lastSuccessAt:i.timestamp({withTimezone:!0,pgName:`last_success_at`}),lastError:i.varchar({length:1024,pgName:`last_error`}),lastCreated:i.integer({notNull:!0,default:0,pgName:`last_created`}),lastUpdated:i.integer({notNull:!0,default:0,pgName:`last_updated`}),lastRetired:i.integer({notNull:!0,default:0,pgName:`last_retired`}),lastUnchanged:i.integer({notNull:!0,default:0,pgName:`last_unchanged`})},primaryKey:`resource`});export{c as A,d as C,m as D,u as E,l as M,s as N,x as O,o as P,b as S,y as T,w as _,R as a,g as b,F as c,P as d,A as f,C as g,O as h,H as i,p as j,f as k,L as l,T as m,B as n,z as o,M as p,V as r,I as s,U as t,N as u,_ as v,S as w,h as x,v as y};
|
|
2
|
+
//# sourceMappingURL=sync-state-table-Dq_wyKfo.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-state-table-Dq_wyKfo.mjs","names":[],"sources":["../src/constants.ts","../src/config.ts","../src/entities/guard.ts","../src/entities/legal-document.ts","../src/entities/room-type.ts","../src/entities/site-info.ts","../src/entities/index.ts","../src/sync-state-table.ts"],"sourcesContent":["/**\n * Names, identifiers and stated numbers for the yhikas-admin sync.\n *\n * Pure module — no imports, no side effects. Both the jiti-loaded plugin entry\n * and the worker-side job modules read from here, so nothing in this file may\n * reach a `server-only` module.\n */\n\n/** Plugin name, as it appears in `app.plugins` and `Plugin.requires`. */\nexport const YHIKAS_SYNC_PLUGIN_NAME = '@murumets-ee/yhikas-sync'\n\n/**\n * The synthetic actor every sync write is attributed to.\n *\n * Not `'cli'`. `auditable()` records this in `created_by`/`updated_by`, so a\n * human reading the audit log can tell a sync write from an operator running a\n * `lumi` command — and the machine-written guard (see `entities/guard.ts`) can\n * refuse every other writer by name. Fits `varchar(255)`; user ids in this\n * codebase are varchar, never uuid.\n */\nexport const YHIKAS_SYNC_ACTOR_ID = 'yhikas-sync'\n\n/**\n * Job names. `JOB_NAME_RE` in `@murumets-ee/queue` is\n * `/^[a-zA-Z0-9:_-]{1,128}$/` — colons are the `plugin:action` separator and\n * dots are rejected outright.\n */\nexport const SYNC_JOB_NAME = 'yhikas-sync:pull'\nexport const WATCHDOG_JOB_NAME = 'yhikas-sync:staleness-watchdog'\n\n/**\n * The three resources in scope.\n *\n * `site_info` was deliberately absent until 2026-08-11 (D027). PR 05 left it\n * out on a MEASUREMENT — `site_info`/`site_notice` appear only in\n * yhikas-admin's `src/db/schema.ts` and its public route, with no admin UI, no\n * server action, no seed and no internal consumer — from which the plan\n * concluded that no rows existed and a sync would pull nothing.\n *\n * Measured against production on 2026-08-11 (R028), that premise had expired:\n * the route answers 200 with populated reception hours and two live notices.\n * The rows were written outside the app. Hardcoding them in the rebuilt site\n * would therefore have frozen live data, not stood in for an empty upstream.\n */\nexport const ROOM_TYPES_RESOURCE = 'room_types'\nexport const LEGAL_DOCUMENTS_RESOURCE = 'legal_documents'\nexport const SITE_INFO_RESOURCE = 'site_info'\n\nexport const SYNC_RESOURCES = [\n ROOM_TYPES_RESOURCE,\n LEGAL_DOCUMENTS_RESOURCE,\n SITE_INFO_RESOURCE,\n] as const\nexport type SyncResource = (typeof SYNC_RESOURCES)[number]\n\n/**\n * The business key of the one `yhikas_site_info` row.\n *\n * Upstream `site_info` is a singleton and its notices carry no id or code of\n * their own — only `{text, isActive, order}` — so there is no natural key to\n * diff N rows on. Modelling it as ONE row with a constant key keeps the whole\n * package on a single identity story (`code`, `type`, and now `key`), and\n * keeps the notices ordered without inventing an identity that upstream would\n * not preserve across an edit.\n */\nexport const SITE_INFO_KEY = 'default'\n\n/**\n * Locale codes. Upstream `MultilingualText` is `{ en, et }` and the lumi codes\n * are identical, so there is no mapping to configure — inventing one would be\n * a seam for a second customer this package does not have (N1).\n */\nexport const ET_LOCALE = 'et'\nexport const EN_LOCALE = 'en'\n\n/**\n * Currency and VAT treatment, DECLARED here because they are not data upstream\n * (R009 §6 / R013): `room_type` has no currency column and no tax column, and\n * VAT is computed at invoice time against Merit article codes, never stored.\n * Stating them makes a mis-entered source value render as a visibly wrong\n * number against a declared unit rather than as a plausible one.\n *\n * The third semantic — the period — stays encoded in the field NAME\n * (`monthlyRent` / `dailyRent`), because one row carries two different periods\n * and a single `period` column could only be wrong about one of them.\n */\nexport const DECLARED_CURRENCY = 'EUR'\n/** `net` = the amount excludes VAT. */\nexport const DECLARED_VAT_TREATMENT = 'net'\n\n/**\n * D017. Four successes a day against a fee schedule revised at most annually,\n * and an alert only after four consecutive failures.\n *\n * The interval form rather than a cron expression is deliberate: the queue's\n * cron parser is UTC-only with no timezone field and no DST awareness (R012\n * §4), so a wall-clock schedule would silently shift twice a year.\n */\nexport const DEFAULT_SYNC_SCHEDULE = '@every 6h'\nexport const DEFAULT_WATCHDOG_SCHEDULE = '@every 1h'\nexport const DEFAULT_STALE_AFTER_MS = 24 * 60 * 60 * 1000\n\n/** The upstream limiter allows 60/min; a run makes three calls. A 15s deadline is generous. */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 15_000\n\n/**\n * Hard ceiling on snapshot size. A snapshot ABOVE this refuses the run rather\n * than processing a prefix — truncating would turn the dropped tail into\n * apparent absences and therefore into mass retirement (D016).\n * ~25 room types today; 500 leaves three orders of magnitude of headroom.\n */\nexport const DEFAULT_MAX_ROWS_PER_RESOURCE = 500\n\n/** D016's sanity floor: refuse a run retiring more than half of at least this many published rows. */\nexport const DEFAULT_MAX_RETIRE_FRACTION = 0.5\nexport const DEFAULT_MIN_ROWS_FOR_RETIRE_FLOOR = 3\n\n/** Upstream paths. GET-only, by construction — there is no write path to name. */\nexport const ROOM_TYPES_PATH = '/api/public/room-types'\nexport const LEGAL_DOCUMENTS_PATH = '/api/public/legal-documents'\nexport const SITE_INFO_PATH = '/api/public/site-info'\n\n/**\n * Hard ceiling on ticker items. A ticker is a marquee: past a couple of dozen\n * entries nobody reads the tail, and an unbounded array from upstream would\n * ride into a JSONB column and out onto every page of the site.\n */\nexport const MAX_SITE_NOTICES = 25\n\n/**\n * Env var carrying the sync's OWN bearer credential — deliberately not the\n * site's `PUBLIC_SITE_API_KEY`. The upstream limiter buckets on\n * `sha256(key).slice(0,16)` rather than on the caller IP (there is no\n * `x-forwarded-for` over the Docker-internal path), so sharing a key means\n * sharing one 60/min bucket, and revoking one credential would blind both.\n */\nexport const API_KEY_ENV_VAR = 'YHIKAS_SYNC_API_KEY'\nexport const BASE_URL_ENV_VAR = 'YHIKAS_ADMIN_BASE_URL'\n","/**\n * Plugin configuration — pure resolution, no I/O, no `server-only`.\n *\n * There is deliberately no `defaultLocale` knob either. The default locale is\n * read from `@murumets-ee/content`'s `resolveI18nConfig(app)` at job-run time,\n * because a configured copy could silently disagree with the app's actual\n * default — and the consequences run both ways: `updateForLocale` would take\n * its default-locale branch for what this package believed was the secondary\n * locale and overwrite the base row with the wrong language, while no\n * translation was written for the real one. Neither write fails. A knob whose\n * only reachable states are \"correct but redundant\" and \"wrong and silent\" is a\n * trap, not a setting.\n *\n * The bearer credential is deliberately NOT part of this object. Resolved\n * config is stored in the framework's `PluginConfigRegistry`, which is a\n * process-global any plugin can read; a secret belongs in the environment and\n * is read at job-run time instead (`resolveCredential`). This mirrors\n * `@murumets-ee/merit` D003 — the key never lands in a registry or the DB.\n */\n\nimport {\n BASE_URL_ENV_VAR,\n DEFAULT_MAX_RETIRE_FRACTION,\n DEFAULT_MAX_ROWS_PER_RESOURCE,\n DEFAULT_MIN_ROWS_FOR_RETIRE_FLOOR,\n DEFAULT_REQUEST_TIMEOUT_MS,\n DEFAULT_STALE_AFTER_MS,\n DEFAULT_SYNC_SCHEDULE,\n DEFAULT_WATCHDOG_SCHEDULE,\n} from './constants.js'\n\nexport interface YhikasSyncConfig {\n /**\n * Origin of the yhikas-admin deployment, e.g. `http://yhikas-admin:3000`\n * over the Docker-internal network. Falls back to `YHIKAS_ADMIN_BASE_URL`.\n */\n baseUrl?: string\n /** Cron/interval expression for the pull. Default `@every 6h` (D017). */\n schedule?: string\n /** Cron/interval expression for the staleness watchdog. Default `@every 1h`. */\n watchdogSchedule?: string\n /** Alert when the newest successful sync is older than this. Default 24h. */\n staleAfterMs?: number\n /** Per-request deadline. The queue has NO per-job timeout (R012 §3). Default 15s. */\n requestTimeoutMs?: number\n /** Refuse a snapshot larger than this rather than truncating it (D016). */\n maxRowsPerResource?: number\n /** Refuse a run retiring more than this fraction of published rows. Default 0.5. */\n maxRetireFraction?: number\n /** The fraction rule only applies once at least this many rows are published. Default 3. */\n minRowsForRetireFloor?: number\n /**\n * Explicit opt-out. An unconfigured sync is a BROKEN sync, and a broken sync\n * must be visibly broken (S7) — so an absent credential fails the job loudly\n * rather than quietly skipping registration. Set this to `false` when the\n * install genuinely has no yhikas-admin to talk to.\n */\n enabled?: boolean\n /**\n * Who draws a room's and a legal document's detail page.\n *\n * The default is the toolkit's: each entity's stored detail template\n * (`drawnBy: 'template'`, block-library D009), which also narrows the public\n * content API to what the template draws. A site that draws those pages with\n * hand-built routes says so here, WITH a reason — the opt-out is reported at\n * start-up (`handBuiltDetailPages`), so it is a recorded decision rather\n * than a default (block-library PR19).\n *\n * @example\n * yhikasSync({ detailPages: { drawnBy: 'site', reason: 'Hand-built Afišš room and document pages (owner, 2026-10-02)' } })\n */\n detailPages?:\n | { readonly drawnBy: 'template' }\n | { readonly drawnBy: 'site'; readonly reason: string }\n}\n\nexport interface ResolvedYhikasSyncConfig {\n readonly baseUrl: string | undefined\n readonly schedule: string\n readonly watchdogSchedule: string\n readonly staleAfterMs: number\n readonly requestTimeoutMs: number\n readonly maxRowsPerResource: number\n readonly maxRetireFraction: number\n readonly minRowsForRetireFloor: number\n readonly enabled: boolean\n readonly detailPages:\n | { readonly drawnBy: 'template' }\n | { readonly drawnBy: 'site'; readonly reason: string }\n}\n\n/** Thrown at plugin construction for a structurally invalid config. */\nexport class YhikasSyncConfigError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'YhikasSyncConfigError'\n }\n}\n\n/**\n * Row counts must be whole. `maxRowsPerResource` becomes a query `limit`,\n * which Postgres rejects when fractional — and it would fail at fetch time\n * with an error naming neither the config field nor the reason. Caught at the\n * boundary instead, where the message can say which knob is wrong.\n */\nfunction requirePositiveInteger(value: number, field: string): number {\n if (!Number.isSafeInteger(value) || value <= 0) {\n throw new YhikasSyncConfigError(`${field} must be a positive integer, got ${value}`)\n }\n return value\n}\n\nfunction requirePositive(value: number, field: string): number {\n if (!Number.isFinite(value) || value <= 0) {\n throw new YhikasSyncConfigError(`${field} must be a finite positive number, got ${value}`)\n }\n return value\n}\n\n/**\n * Resolve config + environment into the frozen object the plugin declares.\n *\n * Throws only on a STRUCTURALLY invalid config — a nonsensical number. It never\n * throws for a missing credential or a missing base URL: `lumi.config.ts` is\n * loaded at build time by the CLI, and throwing there breaks every `lumi`\n * command on a machine that simply has no secrets (see\n * `gotcha_lumi_config_throw_at_build_time`). A missing credential surfaces at\n * job-run time, where it becomes an alert.\n */\nexport function resolveYhikasSyncConfig(\n config: YhikasSyncConfig = {},\n env: NodeJS.ProcessEnv = process.env,\n): ResolvedYhikasSyncConfig {\n const maxRetireFraction = config.maxRetireFraction ?? DEFAULT_MAX_RETIRE_FRACTION\n if (!Number.isFinite(maxRetireFraction) || maxRetireFraction <= 0 || maxRetireFraction > 1) {\n throw new YhikasSyncConfigError(\n `maxRetireFraction must be in (0, 1], got ${maxRetireFraction}. A value of 1 disables the ` +\n `fraction rule; the empty-snapshot rule still applies.`,\n )\n }\n\n const rawBaseUrl = config.baseUrl ?? env[BASE_URL_ENV_VAR]\n\n return Object.freeze({\n baseUrl: rawBaseUrl && rawBaseUrl.length > 0 ? rawBaseUrl : undefined,\n schedule: config.schedule ?? DEFAULT_SYNC_SCHEDULE,\n watchdogSchedule: config.watchdogSchedule ?? DEFAULT_WATCHDOG_SCHEDULE,\n staleAfterMs: requirePositive(config.staleAfterMs ?? DEFAULT_STALE_AFTER_MS, 'staleAfterMs'),\n requestTimeoutMs: requirePositive(\n config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,\n 'requestTimeoutMs',\n ),\n maxRowsPerResource: requirePositiveInteger(\n config.maxRowsPerResource ?? DEFAULT_MAX_ROWS_PER_RESOURCE,\n 'maxRowsPerResource',\n ),\n maxRetireFraction,\n minRowsForRetireFloor: requirePositiveInteger(\n config.minRowsForRetireFloor ?? DEFAULT_MIN_ROWS_FOR_RETIRE_FLOOR,\n 'minRowsForRetireFloor',\n ),\n enabled: config.enabled ?? true,\n detailPages: resolveDetailPages(config.detailPages),\n })\n}\n\nfunction resolveDetailPages(\n detailPages: YhikasSyncConfig['detailPages'],\n): ResolvedYhikasSyncConfig['detailPages'] {\n if (detailPages === undefined || detailPages.drawnBy === 'template') {\n return Object.freeze({ drawnBy: 'template' as const })\n }\n if (detailPages.drawnBy !== 'site') {\n throw new YhikasSyncConfigError(\n `detailPages.drawnBy must be 'template' or 'site', got ${String(\n (detailPages as { drawnBy?: unknown }).drawnBy,\n )}.`,\n )\n }\n const reason = typeof detailPages.reason === 'string' ? detailPages.reason.trim() : ''\n if (reason === '') {\n throw new YhikasSyncConfigError(\n \"detailPages: { drawnBy: 'site' } needs a `reason` — why this site draws room and \" +\n 'document pages by hand instead of with their detail templates.',\n )\n }\n return Object.freeze({ drawnBy: 'site' as const, reason })\n}\n","/**\n * The machine-written guard.\n *\n * PR 05's scope, verbatim: \"synced content is NOT editor-writable — an edit\n * here silently reverts on the next run, which is worse than no editor.\" A\n * field an editor can change and the next sync silently overwrites is a worse\n * affordance than a read-only one, because it looks like it worked.\n *\n * ## Why this is a hook and not `access`\n *\n * `defineEntity`'s `access` is advisory metadata — the firewall\n * (`buildPermissionChecker`) does not read it, and the `admin` role always\n * returns `true` regardless. `admin.hidden` only removes an entity from the\n * nav and is explicitly not a security control. So neither can distinguish the\n * sync from a human administrator; both elevate to the same always-true\n * checker.\n *\n * What CAN distinguish them is the actor identity, which `auditable()` already\n * threads through `BehaviorContext`. The sync runs under a named synthetic\n * actor ({@link YHIKAS_SYNC_ACTOR_ID}), so this hook admits exactly that\n * identity and refuses every other writer — including an administrator\n * clicking through the generic CRUD UI.\n *\n * The `order.ts` precedent uses `ctx.viaInternal` instead. That is deliberately\n * NOT copied here: `viaInternal` is set only by `AdminClient.updateInternal`,\n * and the sync's per-locale writes go through `updateForLocale`, which routes\n * to `update(…, { allowBaseWrite: true })` and never sets the flag. A\n * `viaInternal` gate would therefore block the sync's own writes.\n *\n * ## The translation path — covered since PR 12 (F012)\n *\n * `AdminClient.saveTranslation` and `deleteTranslation` run **no behaviour\n * hooks** — both do a `resolveAuthContext(…, 'update')` permission check and\n * then write directly. And `updateForLocale` reaches `update()` (and therefore\n * the three hooks above) only when the payload leaves non-empty base fields, so\n * a translation-only payload never touched them either.\n *\n * That left this guard covering base rows and nothing else: an administrator\n * who switched the content-locale switcher to `en` and edited a synced document\n * wrote successfully, unseen. And the sync would not revert it, because\n * `sourceHash` is computed from the UPSTREAM projection and never from local\n * state — so the differ read the row as `unchanged` and never wrote it again.\n * The English text diverged from yhikas-admin permanently, with no alert, no\n * staleness and no revert. That is strictly worse than the silent-revert\n * failure this guard was built to prevent.\n *\n * `Behavior.assertTranslationWrite` closes it. It is the sibling of\n * `assertBulkUpdate` and exists for the same reason — a write path that runs no\n * hooks needs its own guard slot — but it carries the resolved\n * `BehaviorContext`, because the question here is WHO is writing rather than\n * WHICH columns.\n *\n * It covers THREE doors, not two, and the third was found by review after the\n * first two were closed: `updateForLocale({ status })` writes\n * `<entity>_locale_status`, a different table again, and a payload of exactly\n * `{ status }` leaves both the translatable and base field sets empty — so\n * neither `saveTranslation` nor `update` runs and neither of their guards\n * fires. Unpublishing one locale of a synced document is the same harm as\n * editing its text and just as permanent: the base row is untouched, so\n * `sourceHash` still matches and the differ reports `unchanged` forever.\n *\n * So `assertSyncActor` now runs on create, update, delete, bulk update, both\n * translation writes, and the per-locale status write.\n *\n * The one honest limit that remains: anything that can mint an elevated context\n * with this actor id can write these rows. That is the whole of the toolkit's\n * elevation surface, and narrowing it further is a firewall question, not an\n * entity one.\n */\n\nimport type { Behavior, BehaviorContext } from '@murumets-ee/entity'\nimport { YHIKAS_SYNC_ACTOR_ID } from '../constants.js'\n\n/** Thrown when a writer other than the sync tries to mutate synced content. */\nexport class YhikasSyncedContentReadOnlyError extends Error {\n constructor(\n entityName: string,\n action:\n | 'create'\n | 'update'\n | 'delete'\n | 'bulk update'\n | 'translation write'\n | 'locale publish change',\n actorId: string,\n ) {\n // The CONSEQUENCE differs by door, and saying the wrong one sends an\n // operator looking for the wrong thing. A base-row edit really would be\n // reverted on the next run. A translation or per-locale publish edit would\n // NOT be: `sourceHash` is computed from the upstream projection and never\n // from local state, so the differ reports `unchanged` and the divergence\n // stands permanently. That asymmetry is the whole reason this guard grew\n // past the base row, so the message must not flatten it back.\n const consequence =\n action === 'translation write' || action === 'locale publish change'\n ? `a change here would NOT be reverted on the next sync run — it does not alter ` +\n `sourceHash, so the differ reports the row unchanged and the divergence from ` +\n `yhikas-admin stands permanently, with no alert and no staleness`\n : `an edit here would be silently reverted on the next sync run, which is worse than ` +\n `no editor at all`\n super(\n `'${entityName}' is written only by the yhikas-admin sync. Refusing ${action} by ` +\n `'${actorId}': ${consequence}. Change the value in yhikas-admin instead.`,\n )\n this.name = 'YhikasSyncedContentReadOnlyError'\n }\n}\n\nfunction assertSyncActor(\n entityName: string,\n action: 'create' | 'update' | 'delete' | 'translation write' | 'locale publish change',\n ctx: BehaviorContext | undefined,\n): void {\n // A missing context is refused, not waved through. An unattributable write to\n // synced content is exactly the case this guard exists for, and defaulting to\n // allow would make the guard depend on every future call path remembering to\n // populate `ctx`.\n const actorId = ctx?.user?.id\n if (actorId !== YHIKAS_SYNC_ACTOR_ID) {\n throw new YhikasSyncedContentReadOnlyError(entityName, action, actorId ?? '<unattributed>')\n }\n}\n\n/**\n * Refuse every create/update/delete not performed by the sync actor.\n *\n * @param entityName Used only in the refusal message — the hook context does\n * not carry the entity name in a shape this can rely on.\n */\nexport function machineWritten<N extends string>(\n entityName: N,\n): Behavior<Record<never, never>, `yhikas-sync:machine-written:${N}`> {\n return {\n name: `yhikas-sync:machine-written:${entityName}` as const,\n hooks: {\n beforeCreate: async (data, ctx) => {\n assertSyncActor(entityName, 'create', ctx)\n return data\n },\n beforeUpdate: async (_id, data, ctx) => {\n assertSyncActor(entityName, 'update', ctx)\n return data\n },\n beforeDelete: async (_id, ctx) => {\n assertSyncActor(entityName, 'delete', ctx)\n },\n },\n // `AdminClient.updateMany` runs NO per-row hooks by design, so the three\n // hooks above cannot see it — a bulk write would walk straight past the\n // guard. `assertBulkUpdate` is the only thing that fires on that path, and\n // `BulkUpdateInfo` is deliberately shape-only: it carries the column set,\n // never an actor. So there is no identity to test, and the honest response\n // is to refuse the path outright. The sync never uses `updateMany`, so\n // that costs nothing and closes the hole rather than leaving it open for\n // want of something to check.\n assertBulkUpdate: () => {\n throw new YhikasSyncedContentReadOnlyError(entityName, 'bulk update', '<bulk>')\n },\n // The translation write path (`saveTranslation` / `deleteTranslation`) runs\n // no hooks either, but UNLIKE the bulk path it carries an actor — so this\n // one tests identity rather than refusing outright. It has to: the sync's\n // own per-locale writes go through `updateForLocale`, which routes a\n // translation-only payload straight to `saveTranslation`. A blanket refusal\n // here would block the sync itself, which is precisely the mistake the\n // `viaInternal` note above records for the base path.\n assertTranslationWrite: (info, ctx) => {\n // The action is named in the refusal because the three doors need\n // different remedies in an operator's head: two are \"you edited the\n // text\", the third is \"you unpublished a language\".\n assertSyncActor(\n entityName,\n info.action === 'locale-status' ? 'locale publish change' : 'translation write',\n ctx,\n )\n },\n }\n}\n","/**\n * `yhikas_legal_document` — the local projection of yhikas-admin's\n * `legal_document` (active rows only; the upstream route filters\n * `is_active = true` server-side so an unpublished draft cannot leak).\n *\n * ## The body is HTML, and it is sanitized on the way in\n *\n * Upstream stores raw TipTap-authored HTML in parallel `html_content_et` /\n * `html_content_en` columns, so this side stores and renders trusted HTML\n * rather than markdown. Every value passes through `@murumets-ee/blocks`'\n * allowlist-based `sanitizeHtml()` at INGEST, not at render: sanitizing at the\n * boundary means the stored bytes are already safe, so a future reader that\n * forgets to sanitize is not a stored-XSS hole.\n *\n * \"It comes from our own admin\" is not a reason to skip it — F006 is precisely\n * why. The upstream price mutations are unauthenticated, which means the\n * premise that this content is trusted is weaker than it looks, and the sync\n * would propagate whatever it was handed with complete fidelity.\n *\n * ## Security\n *\n * Same two controls as `yhikas_room_type`: deny-by-default for every non-admin\n * role, plus `machineWritten` refusing any writer that is not the sync actor.\n * `access` is advisory metadata only.\n */\n\nimport { auditable, defineEntity, field, publishable } from '@murumets-ee/entity'\nimport { machineWritten } from './guard.js'\n\nexport const YHIKAS_LEGAL_DOCUMENT_ENTITY = 'yhikas_legal_document'\n\nexport const YhikasLegalDocument = defineEntity({\n name: YHIKAS_LEGAL_DOCUMENT_ENTITY,\n scope: 'global',\n // A legal document has its own page, drawn by the stored \"legal document\n // detail\" template (block-library D009).\n //\n // ⚠️ The path uses `sourceSlug`, which keeps the URLs yhikas-admin already\n // publishes — and `sourceSlug` is deliberately NOT unique (see the field).\n // A path, however, IS unique per locale: if two upstream documents ever\n // exchange slugs, the first write of that run claims a path the other still\n // holds and is refused, so that run fails until the slugs are untangled\n // upstream. `type` would never collide but would change every document URL.\n //\n // The same guard now sits in front of EVERY sync write of a routable row, so\n // a write is also refused when its path is already held by another document\n // (a page someone authored at `/dokumendid/<slug>`) or falls under a reserved\n // prefix. There is no slug to auto-suffix: the refusal is the outcome.\n frontend: {\n path: { et: '/dokumendid/[sourceSlug]', en: '/documents/[sourceSlug]' },\n drawnBy: 'template',\n },\n writtenBy: 'machine',\n fields: {\n /**\n * The business key. `legal_document.type` is `text().notNull().unique()`\n * upstream — NOT a pgEnum, and there is no TS union anywhere. The five\n * values seeded today are closed by convention only, so nothing here\n * validates membership in a list: doing so would turn a newly authored\n * upstream document into a hard sync failure.\n */\n type: field.text({ required: true, unique: true, indexed: true, maxLength: 190 }),\n\n title: field.text({ required: true, translatable: true }),\n\n /**\n * The upstream slug, verbatim — deliberately `field.text` and not\n * `field.slug`, which would regenerate it from a source field and silently\n * diverge from the anchor links yhikas-admin already publishes.\n *\n * Indexed but NOT unique: it is unique upstream, but the sync writes rows\n * one at a time, so a run in which two documents exchange slugs would hit\n * a transient collision mid-run and fail a sync that is not actually\n * invalid. `type` is the identity and carries the uniqueness guarantee.\n */\n sourceSlug: field.text({ required: true, indexed: true, maxLength: 190 }),\n\n /** Sanitized HTML. Translatable: `et` on the base row, `en` in the translations table. */\n body: field.richtext({ required: true, translatable: true }),\n\n /** Upstream display order. */\n order: field.number({ integer: true, required: true, default: 0 }),\n\n /**\n * Whether the upstream row carried non-empty English content. A\n * convenience for the renderer — the control is per-locale publish status\n * (D015). Serving Estonian legal text under an English URL is the failure\n * this exists to prevent, and it is worse than the document being absent:\n * rental terms and fire-safety instructions read as correct English-locale\n * content to every automated check.\n */\n hasEnglish: field.boolean({ required: true, default: false }),\n\n /**\n * Stable hash of the normalized upstream row. `legal_document` DOES carry\n * an `updated_at` — set by hand rather than by `$onUpdate` — but the public\n * route deliberately excludes it from the projection, so it is not\n * available as a change signal on this side.\n */\n sourceHash: field.text({ maxLength: 64 }),\n },\n behaviors: [auditable(), publishable(), machineWritten(YHIKAS_LEGAL_DOCUMENT_ENTITY)],\n access: {\n view: 'group.admin',\n create: 'group.admin',\n update: 'group.admin',\n delete: 'group.admin',\n },\n admin: {\n group: 'yhikas',\n label: 'Legal documents',\n labelSingular: 'Legal document',\n titleField: 'type',\n icon: 'file-text',\n description:\n 'Synced from yhikas-admin every 6 hours. Read-only here — an edit would be reverted on the next run. Change the text in yhikas-admin.',\n defaultSort: 'order',\n defaultSortDirection: 'asc',\n disableCreate: true,\n hideFromDashboard: true,\n hiddenFields: ['sourceHash'],\n hiddenColumns: ['sourceHash'],\n },\n})\n","/**\n * `yhikas_room_type` — the local projection of yhikas-admin's `room_type`.\n *\n * ## Decimals are text, and that is not a style choice\n *\n * `field.number()` maps to Postgres `doublePrecision` unless `integer: true`\n * — a float8. Every money and area value here arrives from `pg` as a STRING\n * (`numeric` serialization) and is stored as one, because routing a price\n * through a binary float introduces rounding that nothing downstream can undo.\n * `capacity` and `placesOccupied` are genuine integers and use `field.number`.\n *\n * ## Nulls are data\n *\n * Upstream passes nulls through untouched and says why: \"so the site can\n * decide how to render a missing price rather than displaying a fabricated\n * zero.\" None of the price fields is `required`, and the sync substitutes no\n * defaults — a missing price stays missing all the way to the renderer.\n *\n * ## What this side declares\n *\n * There is no currency column, no VAT column and no period column upstream;\n * EUR-net-per-month is a convention held in column NAMING. `currency` and\n * `vatTreatment` are therefore real columns written with constants, so a\n * mis-entered source value renders as a visibly wrong number against a\n * declared unit rather than as a plausible one.\n *\n * ## Security\n *\n * `access` below is advisory metadata — the firewall does not read it and the\n * `admin` role bypasses it unconditionally. The real controls are (a)\n * deny-by-default: `buildInitialRoleDefinitions` grants no non-admin role any\n * permission on this entity, and (b) `machineWritten`, which refuses every\n * writer that is not the sync actor, including an administrator in the CRUD\n * UI. `admin.disableCreate` is a UI affordance, never a gate.\n */\n\nimport { auditable, defineEntity, field, publishable } from '@murumets-ee/entity'\nimport { DECLARED_CURRENCY, DECLARED_VAT_TREATMENT } from '../constants.js'\nimport { machineWritten } from './guard.js'\n\nexport const YHIKAS_ROOM_TYPE_ENTITY = 'yhikas_room_type'\n\n/** VAT treatments this projection can express. `net` = the amount excludes VAT. */\nexport const VAT_TREATMENTS = ['net', 'gross'] as const\n\nexport const YhikasRoomType = defineEntity({\n name: YHIKAS_ROOM_TYPE_ENTITY,\n scope: 'global',\n // A room has its own page, at the URLs the site already uses, and that page\n // is the stored \"room detail\" template drawn with this row (block-library\n // D009) — the entity has no block field, because nobody authors a room here.\n // `code` is unique, so two rooms can never claim one path. A sync write is\n // still refused if ANOTHER document already holds that path (a page authored\n // at `/toad/<code>`) or it falls under a reserved prefix — being routable\n // puts the path guard in front of every write of this row.\n //\n // `drawnBy` also NARROWS what the public content API serves for a room to its\n // id plus the values the site's detail bindings name; see `EntityFrontend`.\n frontend: { path: { et: '/toad/[code]', en: '/rooms/[code]' }, drawnBy: 'template' },\n // Stated so the toolkit can SEE what `machineWritten()` below enforces: the\n // canvas shows a room's values read-only and says why, instead of accepting\n // an edit the guard then refuses.\n writtenBy: 'machine',\n fields: {\n /**\n * The business key. `room_type.code` is `notNull().unique()` upstream and\n * is what identity is keyed on — never the serial `id`, which is an\n * implementation detail of the other system and would make this system's\n * content depend on the other's insert order.\n */\n code: field.text({ required: true, unique: true, indexed: true, maxLength: 190 }),\n\n /** Translatable: `et` on the base row, `en` in `yhikas_room_type_translations`. */\n name: field.text({ required: true, translatable: true }),\n\n /** Areas in m², as strings. See the note on decimals above. */\n totalArea: field.text({ maxLength: 32 }),\n livingArea: field.text({ maxLength: 32 }),\n commonArea: field.text({ maxLength: 32 }),\n\n capacity: field.number({ integer: true }),\n placesOccupied: field.number({ integer: true }),\n\n /**\n * Period is carried in the field NAME rather than in a `period` column,\n * deliberately: one row holds both a monthly and a daily rate, so a single\n * period column could only ever be wrong about one of them.\n */\n monthlyRent: field.text({ maxLength: 32 }),\n discountedMonthlyRent: field.text({ maxLength: 32 }),\n dailyRent: field.text({ maxLength: 32 }),\n\n /** Declared by this side; not data upstream. */\n currency: field.text({ required: true, maxLength: 3, default: DECLARED_CURRENCY }),\n vatTreatment: field.select({\n options: VAT_TREATMENTS,\n required: true,\n default: DECLARED_VAT_TREATMENT,\n }),\n\n /**\n * Whether the upstream row carried a non-empty English name. A convenience\n * for a renderer building an English price sheet — NOT the control. The\n * control is per-locale publish status (D015): when this is `false` the\n * `en` locale is unpublished, so the public read path cannot serve it\n * whether or not anyone reads this flag.\n */\n hasEnglish: field.boolean({ required: true, default: false }),\n\n /**\n * Stable hash of the normalized upstream row. `room_type` carries no\n * timestamp of any kind — no `created_at`, no `updated_at`, no version —\n * so this is the only available change signal, and without it every run\n * would rewrite all ~25 rows and fill the audit log with ~100 no-op\n * updates a day.\n */\n sourceHash: field.text({ maxLength: 64 }),\n },\n behaviors: [auditable(), publishable(), machineWritten(YHIKAS_ROOM_TYPE_ENTITY)],\n access: {\n view: 'group.admin',\n create: 'group.admin',\n update: 'group.admin',\n delete: 'group.admin',\n },\n admin: {\n group: 'yhikas',\n label: 'Room types',\n labelSingular: 'Room type',\n titleField: 'code',\n icon: 'bed-double',\n description:\n 'Synced from yhikas-admin every 6 hours. Read-only here — an edit would be reverted on the next run. Change prices in yhikas-admin.',\n defaultSort: 'code',\n defaultSortDirection: 'asc',\n disableCreate: true,\n hideFromDashboard: true,\n hiddenFields: ['sourceHash'],\n hiddenColumns: ['sourceHash'],\n },\n})\n","/**\n * `yhikas_site_info` — reception hours and the notice ticker, as ONE row.\n *\n * ## Why a singleton and not N notice rows\n *\n * Upstream `site_notice` carries `{text, isActive, order}` and nothing else —\n * no id, no code, no timestamp. So there is no business key an edit upstream\n * would preserve, and a diff keyed on `order` would read \"notice 2 was\n * reworded\" and \"notices 2 and 3 swapped places\" as the same event. Modelling\n * the resource as one row carrying an ordered list keeps identity honest: the\n * whole list is the value, and it is replaced as a value.\n *\n * That also matches what the site does with it. A ticker renders the list in\n * order; nothing links to an individual notice, so nothing needs one to have a\n * URL, a version or a publish state of its own.\n *\n * ## Both halves are optional, and that is the point\n *\n * `receptionHours` and `notices` can each legitimately be empty — a dormitory\n * with no current notices is an ordinary state. What is NOT ordinary is BOTH\n * being empty, because that is exactly what `/api/public/site-info` returns\n * when no row exists at all (F015), and it is indistinguishable at the wire\n * from a deliberate clearing. `projectSiteInfo` refuses that payload rather\n * than writing it (D027) — the refusal lives there because emptiness is a\n * content judgement, not a schema one.\n *\n * ## Security\n *\n * As with the other two synced entities: `access` is advisory metadata, the\n * firewall does not read it, and `admin` bypasses it unconditionally. The real\n * controls are deny-by-default (no non-admin role is granted anything here) and\n * `machineWritten`, which since PR 12 refuses non-sync writers on the base row\n * AND on the translation path.\n */\n\nimport { auditable, defineEntity, field, publishable } from '@murumets-ee/entity'\nimport { machineWritten } from './guard.js'\n\nexport const YHIKAS_SITE_INFO_ENTITY = 'yhikas_site_info'\n\nexport const YhikasSiteInfo = defineEntity({\n name: YHIKAS_SITE_INFO_ENTITY,\n /**\n * Declared, not merely documented. Everything above argues the singleton\n * semantics and the projection emits exactly one row — but the REGISTRY read\n * `entityKind: \"collection\"`, so `/api/content/v1/yhikas_site_info` answered\n * with the list envelope and any consumer reaching for a singleton read got\n * an array cast to an object, every field `undefined`. On a resource nobody\n * had populated yet that is indistinguishable from working (F056).\n *\n * The framework enforces the cardinality generically (D011): `create` refuses\n * a second live row, `delete` refuses to leave zero. Nothing here restates\n * the rule. Until now the \"exactly one\" guarantee was held only by the\n * projection and by `key`'s unique index — conventional rather than real.\n *\n * ⚠️ WIRE BREAK for any content-API consumer, at UPGRADE time rather than at\n * merge (customer repos pin `@murumets-ee/*` from npm). The envelope changes\n * from `{data:[row],meta}` to `{data:row}`, and `/count`, `/slug/:slug`,\n * `/:id` and every list parameter start refusing. A reader using\n * `listDocuments(…, {limit:1})` must move to `getSingleton()` in the same\n * upgrade step — see this PR's changeset.\n */\n kind: 'singleton',\n scope: 'global',\n fields: {\n /**\n * The business key, constant at {@link SITE_INFO_KEY}.\n *\n * A singleton still needs a key, for the same reason the other two do: the\n * differ matches local to upstream by key, and `applyPlan` upserts on it.\n * A constant key makes the singleton a one-element case of the same\n * machinery rather than a second code path.\n */\n key: field.text({ required: true, unique: true, indexed: true, maxLength: 32 }),\n\n /**\n * Reception hours as free text — `E–R 9–17`, or whatever the office writes.\n *\n * Deliberately NOT structured. Upstream stores one multilingual string and\n * the site renders it verbatim; parsing it into open/close pairs here would\n * invent a schema neither system has, and would fail the first time someone\n * writes \"suletud 24.06\".\n */\n receptionHours: field.text({ translatable: true, maxLength: 500 }),\n\n /**\n * The ticker, as an ordered array of strings for ONE locale.\n *\n * Already filtered to `isActive` and already sorted by `order` at\n * projection time, so a renderer maps over it and nothing more. `isActive`\n * and `order` are upstream's editing affordances, not content — carrying\n * them across would oblige every consumer to re-implement the same filter\n * and sort, and one of them would eventually get it wrong.\n */\n notices: field.json({ translatable: true }),\n\n /**\n * Whether the secondary locale has anything publishable. A convenience\n * flag for the admin list, never the control — per-locale publish status is.\n */\n hasEnglish: field.boolean({ default: false, required: true }),\n\n /** The change signal. Written LAST, so a stale hash means \"redo this row\". */\n sourceHash: field.text({ maxLength: 64 }),\n },\n behaviors: [auditable(), publishable(), machineWritten(YHIKAS_SITE_INFO_ENTITY)],\n access: {\n view: 'group.admin',\n create: 'group.admin',\n update: 'group.admin',\n delete: 'group.admin',\n },\n admin: {\n group: 'yhikas',\n label: 'Site info',\n titleField: 'key',\n disableCreate: true,\n hideFromDashboard: true,\n hiddenFields: ['sourceHash'],\n hiddenColumns: ['sourceHash'],\n },\n})\n","/**\n * The entities this plugin syncs — ONE list, consumed by the plugin's\n * declaration and by the contract tests.\n *\n * Adding `site_info` turned twelve tests red across two files, every one of\n * them because a resource or entity had been re-listed by hand somewhere. This\n * export exists so the next one cannot: `plugin.ts` spreads it into\n * `server.entities`, and `contract.test.ts` iterates it, so a fourth entity is\n * covered by both the moment it is added here.\n */\n\nimport type { ResolvedYhikasSyncConfig } from '../config.js'\nimport { YhikasLegalDocument } from './legal-document.js'\nimport { YhikasRoomType } from './room-type.js'\nimport { YhikasSiteInfo } from './site-info.js'\n\nexport const SYNCED_ENTITIES = [YhikasRoomType, YhikasLegalDocument, YhikasSiteInfo] as const\n\n/**\n * The two entities with a detail page, as the site chose to draw them\n * (`yhikasSync({ detailPages })`, block-library PR19). The default is the\n * definitions themselves; the hand-built opt-out returns copies whose\n * `frontend` says `drawnBy: 'site'` with the site's reason — same name, fields\n * and paths, so rows, path rows and the sync are unchanged; only who draws the\n * page, and what the content API serves, is.\n */\nexport function detailPageEntities(detailPages: ResolvedYhikasSyncConfig['detailPages']): {\n readonly roomType: typeof YhikasRoomType\n readonly legalDocument: typeof YhikasLegalDocument\n} {\n if (detailPages.drawnBy === 'template') {\n return { roomType: YhikasRoomType, legalDocument: YhikasLegalDocument }\n }\n const handBuilt = <E extends typeof YhikasRoomType | typeof YhikasLegalDocument>(\n entity: E,\n ): E => {\n const path = entity.frontend?.path\n if (path === undefined) {\n throw new Error(`yhikas-sync: ${entity.name} declares no frontend path to draw by hand`)\n }\n return { ...entity, frontend: { path, drawnBy: 'site', reason: detailPages.reason } }\n }\n return { roomType: handBuilt(YhikasRoomType), legalDocument: handBuilt(YhikasLegalDocument) }\n}\n\n/** Every synced entity, with the site's choice of who draws the detail pages. */\nexport function syncedEntities(\n detailPages: ResolvedYhikasSyncConfig['detailPages'],\n): readonly (typeof SYNCED_ENTITIES)[number][] {\n if (detailPages.drawnBy === 'template') return SYNCED_ENTITIES\n const { roomType, legalDocument } = detailPageEntities(detailPages)\n return [roomType, legalDocument, YhikasSiteInfo]\n}\n","/**\n * `yhikas_sync_state` — one row per synced resource, recording when that\n * resource last synced successfully.\n *\n * ## Why this table exists at all\n *\n * Phase 05 hoped the job could record success through\n * `@murumets-ee/queue`'s `heartbeats-table.ts` \"rather than inventing a status\n * field\". Measured (R012 §1), that is not available:\n *\n * - `toolkit_worker_heartbeats` is keyed on `workerId` — one row per worker\n * PROCESS. It answers \"is any worker alive\", never \"did this schedule fire\".\n * - `QueueAlerter.recordFailure` is reached from exactly one place,\n * `failJob`'s dead-letter branch, so it fires only on a job that RAN and\n * THREW. A job that never starts produces no error and therefore no alert.\n *\n * The failure S7 requires be observable — a job that silently stops running\n * while the site keeps serving last month's prices — is precisely the one\n * neither can see. So the detection is new; only the alert channel is\n * inherited (the watchdog throws, and the throw reaches the shipped\n * dedupe/digest/email path).\n *\n * This is infrastructure, not content: `defineTable` + `TableClient`, per\n * CLAUDE.md's two-tier rule. It is also the surface a human or a monitor reads\n * to answer \"how stale is the site's price sheet right now\", which is what\n * makes the staleness observable rather than merely bounded.\n */\n\nimport { column, defineTable } from '@murumets-ee/db'\n\nexport const yhikasSyncStateTable = defineTable({\n name: 'yhikas_sync_state',\n columns: {\n /** `room_types` | `legal_documents` — see `SyncResource`. */\n resource: column.varchar({ length: 64, notNull: true }),\n\n /**\n * When this resource was first tracked. The watchdog measures staleness\n * from `lastSuccessAt ?? firstSeenAt`, so a sync that has NEVER once\n * succeeded eventually alerts instead of looking indistinguishable from\n * one that is merely young.\n */\n firstSeenAt: column.timestamp({\n notNull: true,\n withTimezone: true,\n defaultNow: true,\n pgName: 'first_seen_at',\n }),\n\n /** Updated on every run, successful or not. */\n lastAttemptAt: column.timestamp({ withTimezone: true, pgName: 'last_attempt_at' }),\n\n /** Updated ONLY on a fully applied run. This is what the watchdog reads. */\n lastSuccessAt: column.timestamp({ withTimezone: true, pgName: 'last_success_at' }),\n\n /**\n * The last failure's message, cleared on the next success. Bounded, and\n * carries no credential: the client never puts a response body or a key\n * into an error message.\n */\n lastError: column.varchar({ length: 1024, pgName: 'last_error' }),\n\n /** Counts from the last successful run — what a human wants to see first. */\n lastCreated: column.integer({ notNull: true, default: 0, pgName: 'last_created' }),\n lastUpdated: column.integer({ notNull: true, default: 0, pgName: 'last_updated' }),\n lastRetired: column.integer({ notNull: true, default: 0, pgName: 'last_retired' }),\n lastUnchanged: column.integer({ notNull: true, default: 0, pgName: 'last_unchanged' }),\n },\n primaryKey: 'resource',\n})\n"],"mappings":"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"}
|