@clivly/core 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/drizzle.d.cts +1 -1
- package/dist/drizzle.d.mts +1 -1
- package/dist/{entity-config-Dh8aEcgj.cjs → entity-config-Bfq3OkCU.cjs} +36 -12
- package/dist/{entity-config-8Fgr6IOH.mjs → entity-config-DFJ8L9jm.mjs} +31 -13
- package/dist/{entity-config-CLxI8dVb.d.cts → entity-config-buOIVtMM.d.cts} +25 -1
- package/dist/{entity-config-CLxI8dVb.d.mts → entity-config-buOIVtMM.d.mts} +25 -1
- package/dist/entity-config.cjs +2 -1
- package/dist/entity-config.d.cts +2 -2
- package/dist/entity-config.d.mts +2 -2
- package/dist/entity-config.mjs +2 -2
- package/dist/entity-heuristics.cjs +1 -1
- package/dist/entity-heuristics.d.cts +1 -1
- package/dist/entity-heuristics.d.mts +1 -1
- package/dist/entity-heuristics.mjs +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/integration-state.cjs +98 -26
- package/dist/integration-state.d.cts +94 -7
- package/dist/integration-state.d.mts +94 -7
- package/dist/integration-state.mjs +96 -27
- package/dist/mapping-form.d.cts +1 -1
- package/dist/mapping-form.d.mts +1 -1
- package/dist/mappings-config.cjs +25 -1
- package/dist/mappings-config.d.cts +40 -2
- package/dist/mappings-config.d.mts +40 -2
- package/dist/mappings-config.mjs +25 -2
- package/dist/sync-engine.d.cts +1 -1
- package/dist/sync-engine.d.mts +1 -1
- package/dist/view-compiler.d.cts +1 -1
- package/dist/view-compiler.d.mts +1 -1
- package/package.json +1 -1
|
@@ -3,7 +3,13 @@ declare const INTEGRATION_STEPS: readonly ["credential", "application", "schema"
|
|
|
3
3
|
type StepId = (typeof INTEGRATION_STEPS)[number];
|
|
4
4
|
/** `blocked` means "prerequisite unmet", never "broken" — see C-19. */
|
|
5
5
|
type StepState = "done" | "active" | "blocked" | "failed" | "skipped";
|
|
6
|
-
type IntegrationStatus = "not_configured" | "sdk_installed" | "awaiting_connection" | "connected" | "schema_discovered" | "mapping_required" | "trigger_required" | "ready_to_sync" | "syncing" | "healthy" | "warning" | "error" |
|
|
6
|
+
type IntegrationStatus = "not_configured" | "sdk_installed" | "awaiting_connection" | "connected" | "schema_discovered" | "mapping_required" | "trigger_required" | "ready_to_sync" | "syncing" | "healthy" | "warning" | "error" |
|
|
7
|
+
/**
|
|
8
|
+
* @deprecated No longer derived. Liveness is its own dimension — read
|
|
9
|
+
* `ConnectionHealth` from `deriveLiveness`. Setup progress does not move
|
|
10
|
+
* backwards just because an app stopped running.
|
|
11
|
+
*/
|
|
12
|
+
"disconnected";
|
|
7
13
|
declare const INTEGRATION_STEP_LABELS: Record<StepId, string>;
|
|
8
14
|
/** Each step's prerequisites. Ordering is enforced by a test, not convention. */
|
|
9
15
|
declare const STEP_PREREQUISITES: Record<StepId, readonly StepId[]>;
|
|
@@ -13,8 +19,20 @@ interface RunFact {
|
|
|
13
19
|
message?: string;
|
|
14
20
|
ok: boolean;
|
|
15
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Where the reporting app is running, as the app itself understands it.
|
|
24
|
+
*
|
|
25
|
+
* `unknown` is a real value, not an absence: `detectHostEnvironment` refuses to
|
|
26
|
+
* guess when nothing places the app, and a bare `node server.js` with no
|
|
27
|
+
* NODE_ENV must still be able to connect and be shown as itself. Defaulting
|
|
28
|
+
* such a host to `local_dev` would file a production box's faults under local
|
|
29
|
+
* development, where nobody would look for them.
|
|
30
|
+
*/
|
|
31
|
+
type HostEnvironment = "local_dev" | "preview" | "production" | "unknown";
|
|
16
32
|
interface IntegrationFacts {
|
|
17
33
|
discoveredTableCount: number;
|
|
34
|
+
/** The environment the app reported on its last heartbeat. */
|
|
35
|
+
environment?: HostEnvironment | null;
|
|
18
36
|
/**
|
|
19
37
|
* Has this app ever checked in? Distinguishes "never installed" from "was
|
|
20
38
|
* working, now silent" — the same absent heartbeat means different things,
|
|
@@ -22,10 +40,27 @@ interface IntegrationFacts {
|
|
|
22
40
|
*/
|
|
23
41
|
everConnected: boolean;
|
|
24
42
|
hasCredential: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The cadence the app said it heartbeats at. Liveness windows are multiples
|
|
45
|
+
* of this rather than a fixed constant, so an app that deliberately reports
|
|
46
|
+
* slowly is not called dead for obeying its own configuration.
|
|
47
|
+
*/
|
|
48
|
+
heartbeatIntervalMs?: number | null;
|
|
25
49
|
lastHeartbeatAt: number | null;
|
|
26
50
|
lastRun: RunFact | null;
|
|
27
51
|
mappedEntityCount: number;
|
|
28
52
|
now: number;
|
|
53
|
+
/**
|
|
54
|
+
* Deployment presence, when this deployment has one. Absent or `http`
|
|
55
|
+
* transport keeps the cadence-derived behaviour every SDK in the field uses.
|
|
56
|
+
*/
|
|
57
|
+
presence?: DeploymentPresence | null;
|
|
58
|
+
/**
|
|
59
|
+
* Faults the running app diagnosed about itself on its last heartbeat — an
|
|
60
|
+
* unreadable database, a secret missing from the process environment. These
|
|
61
|
+
* are what make a reachable app `live_with_issues`; unfinished setup is not.
|
|
62
|
+
*/
|
|
63
|
+
reportedIssues?: StepProblem[];
|
|
29
64
|
syncInFlight: boolean;
|
|
30
65
|
/** A trigger URL and secret both exist, so Clivly can ask the app to sync. */
|
|
31
66
|
triggerConfigured: boolean;
|
|
@@ -46,6 +81,52 @@ interface IntegrationStep {
|
|
|
46
81
|
state: StepState;
|
|
47
82
|
}
|
|
48
83
|
declare function formatAge(ms: number): string;
|
|
84
|
+
/** The cadence `createClivlySDK` uses when the host app configures none. */
|
|
85
|
+
declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 60000;
|
|
86
|
+
type ConnectionHealth = "live" | "live_with_issues" | "stale" | "offline";
|
|
87
|
+
/** Which algorithm decides whether this deployment is up. */
|
|
88
|
+
type PresenceTransport = "websocket" | "http";
|
|
89
|
+
type PresenceConnectionState = "connected" | "reconnecting" | "disconnected";
|
|
90
|
+
/**
|
|
91
|
+
* A deployment's connection state as its `PresenceRoom` last persisted it.
|
|
92
|
+
*
|
|
93
|
+
* This exists because a socket has an explicit state and a timestamp does not.
|
|
94
|
+
* Deriving WebSocket liveness from heartbeat age would leave a disconnected app
|
|
95
|
+
* shown as live for 150 seconds and offline only after ten minutes, which is
|
|
96
|
+
* the whole reason this transport exists.
|
|
97
|
+
*/
|
|
98
|
+
interface DeploymentPresence {
|
|
99
|
+
/** Sockets currently accepted in the room. */
|
|
100
|
+
activeInstanceCount: number;
|
|
101
|
+
connectionState: PresenceConnectionState;
|
|
102
|
+
transport: PresenceTransport;
|
|
103
|
+
}
|
|
104
|
+
interface ConnectionLiveness {
|
|
105
|
+
/** `"8s ago"`, or `"No heartbeat yet"` when the app has never reported. */
|
|
106
|
+
ageLabel: string;
|
|
107
|
+
ageMs: number | null;
|
|
108
|
+
environment: HostEnvironment | null;
|
|
109
|
+
health: ConnectionHealth;
|
|
110
|
+
/**
|
|
111
|
+
* Faults on a reachable app. Empty unless `health` is `live_with_issues`.
|
|
112
|
+
*/
|
|
113
|
+
issues: StepProblem[];
|
|
114
|
+
/** Present when a deployment-presence record supplied this liveness result. */
|
|
115
|
+
presence?: Pick<DeploymentPresence, "connectionState" | "transport">;
|
|
116
|
+
/** Is a heartbeat recent enough to speak for the app as it is right now? */
|
|
117
|
+
reachable: boolean;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The two boundaries that separate live from stale from offline, in terms of
|
|
121
|
+
* the app's own reported cadence. A cadence we cannot believe — absent, zero,
|
|
122
|
+
* negative — falls back to the SDK default rather than producing a window of
|
|
123
|
+
* zero that would call every app offline.
|
|
124
|
+
*/
|
|
125
|
+
declare function livenessWindows(intervalMs: number | null | undefined): {
|
|
126
|
+
liveMs: number;
|
|
127
|
+
staleMs: number;
|
|
128
|
+
};
|
|
129
|
+
declare function deriveLiveness(facts: IntegrationFacts): ConnectionLiveness;
|
|
49
130
|
/**
|
|
50
131
|
* Walk the ladder once. A step is `blocked` when an earlier step it needs has
|
|
51
132
|
* not been satisfied — a prerequisite statement, not a verdict about this step
|
|
@@ -54,10 +135,9 @@ declare function formatAge(ms: number): string;
|
|
|
54
135
|
*/
|
|
55
136
|
declare function resolveSteps(facts: IntegrationFacts): IntegrationStep[];
|
|
56
137
|
/**
|
|
57
|
-
* Collapse the ladder to the single status the surfaces switch on.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* than how far setup once got.
|
|
138
|
+
* Collapse the ladder to the single status the surfaces switch on. This is the
|
|
139
|
+
* setup dimension only: it answers how far configuration got, never whether the
|
|
140
|
+
* app is up. Pair it with `deriveLiveness` for the second half of the picture.
|
|
61
141
|
*/
|
|
62
142
|
declare function deriveStatus(steps: IntegrationStep[], facts: IntegrationFacts): IntegrationStatus;
|
|
63
143
|
interface NextAction {
|
|
@@ -66,6 +146,13 @@ interface NextAction {
|
|
|
66
146
|
label: string;
|
|
67
147
|
}
|
|
68
148
|
interface IntegrationState {
|
|
149
|
+
/**
|
|
150
|
+
* Is the app running right now. Deliberately alongside `status` rather than
|
|
151
|
+
* folded into it: a surface must be able to say "setup reached mapping" and
|
|
152
|
+
* "the app is offline" in the same breath without either overwriting the
|
|
153
|
+
* other.
|
|
154
|
+
*/
|
|
155
|
+
liveness: ConnectionLiveness;
|
|
69
156
|
next: NextAction | null;
|
|
70
157
|
status: IntegrationStatus;
|
|
71
158
|
steps: IntegrationStep[];
|
|
@@ -82,7 +169,7 @@ declare const INSTALL_COMMAND = "npx clivly init";
|
|
|
82
169
|
* as the fix for a write problem (C-05). If you need a command for the
|
|
83
170
|
* next-action slot, call this — do not write another one.
|
|
84
171
|
*/
|
|
85
|
-
declare function nextAction(steps: IntegrationStep[], status: IntegrationStatus): NextAction | null;
|
|
172
|
+
declare function nextAction(steps: IntegrationStep[], status: IntegrationStatus, liveness: ConnectionLiveness): NextAction | null;
|
|
86
173
|
declare function computeIntegrationState(facts: IntegrationFacts): IntegrationState;
|
|
87
174
|
//#endregion
|
|
88
|
-
export { INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, IntegrationFacts, IntegrationState, IntegrationStatus, IntegrationStep, NextAction, RunFact, STEP_PREREQUISITES, StepId, StepProblem, StepState, computeIntegrationState, deriveStatus, formatAge, nextAction, resolveSteps, stepIndex };
|
|
175
|
+
export { ConnectionHealth, ConnectionLiveness, DEFAULT_HEARTBEAT_INTERVAL_MS, DeploymentPresence, HostEnvironment, INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, IntegrationFacts, IntegrationState, IntegrationStatus, IntegrationStep, NextAction, PresenceConnectionState, PresenceTransport, RunFact, STEP_PREREQUISITES, StepId, StepProblem, StepState, computeIntegrationState, deriveLiveness, deriveStatus, formatAge, livenessWindows, nextAction, resolveSteps, stepIndex };
|
|
@@ -3,7 +3,13 @@ declare const INTEGRATION_STEPS: readonly ["credential", "application", "schema"
|
|
|
3
3
|
type StepId = (typeof INTEGRATION_STEPS)[number];
|
|
4
4
|
/** `blocked` means "prerequisite unmet", never "broken" — see C-19. */
|
|
5
5
|
type StepState = "done" | "active" | "blocked" | "failed" | "skipped";
|
|
6
|
-
type IntegrationStatus = "not_configured" | "sdk_installed" | "awaiting_connection" | "connected" | "schema_discovered" | "mapping_required" | "trigger_required" | "ready_to_sync" | "syncing" | "healthy" | "warning" | "error" |
|
|
6
|
+
type IntegrationStatus = "not_configured" | "sdk_installed" | "awaiting_connection" | "connected" | "schema_discovered" | "mapping_required" | "trigger_required" | "ready_to_sync" | "syncing" | "healthy" | "warning" | "error" |
|
|
7
|
+
/**
|
|
8
|
+
* @deprecated No longer derived. Liveness is its own dimension — read
|
|
9
|
+
* `ConnectionHealth` from `deriveLiveness`. Setup progress does not move
|
|
10
|
+
* backwards just because an app stopped running.
|
|
11
|
+
*/
|
|
12
|
+
"disconnected";
|
|
7
13
|
declare const INTEGRATION_STEP_LABELS: Record<StepId, string>;
|
|
8
14
|
/** Each step's prerequisites. Ordering is enforced by a test, not convention. */
|
|
9
15
|
declare const STEP_PREREQUISITES: Record<StepId, readonly StepId[]>;
|
|
@@ -13,8 +19,20 @@ interface RunFact {
|
|
|
13
19
|
message?: string;
|
|
14
20
|
ok: boolean;
|
|
15
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Where the reporting app is running, as the app itself understands it.
|
|
24
|
+
*
|
|
25
|
+
* `unknown` is a real value, not an absence: `detectHostEnvironment` refuses to
|
|
26
|
+
* guess when nothing places the app, and a bare `node server.js` with no
|
|
27
|
+
* NODE_ENV must still be able to connect and be shown as itself. Defaulting
|
|
28
|
+
* such a host to `local_dev` would file a production box's faults under local
|
|
29
|
+
* development, where nobody would look for them.
|
|
30
|
+
*/
|
|
31
|
+
type HostEnvironment = "local_dev" | "preview" | "production" | "unknown";
|
|
16
32
|
interface IntegrationFacts {
|
|
17
33
|
discoveredTableCount: number;
|
|
34
|
+
/** The environment the app reported on its last heartbeat. */
|
|
35
|
+
environment?: HostEnvironment | null;
|
|
18
36
|
/**
|
|
19
37
|
* Has this app ever checked in? Distinguishes "never installed" from "was
|
|
20
38
|
* working, now silent" — the same absent heartbeat means different things,
|
|
@@ -22,10 +40,27 @@ interface IntegrationFacts {
|
|
|
22
40
|
*/
|
|
23
41
|
everConnected: boolean;
|
|
24
42
|
hasCredential: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The cadence the app said it heartbeats at. Liveness windows are multiples
|
|
45
|
+
* of this rather than a fixed constant, so an app that deliberately reports
|
|
46
|
+
* slowly is not called dead for obeying its own configuration.
|
|
47
|
+
*/
|
|
48
|
+
heartbeatIntervalMs?: number | null;
|
|
25
49
|
lastHeartbeatAt: number | null;
|
|
26
50
|
lastRun: RunFact | null;
|
|
27
51
|
mappedEntityCount: number;
|
|
28
52
|
now: number;
|
|
53
|
+
/**
|
|
54
|
+
* Deployment presence, when this deployment has one. Absent or `http`
|
|
55
|
+
* transport keeps the cadence-derived behaviour every SDK in the field uses.
|
|
56
|
+
*/
|
|
57
|
+
presence?: DeploymentPresence | null;
|
|
58
|
+
/**
|
|
59
|
+
* Faults the running app diagnosed about itself on its last heartbeat — an
|
|
60
|
+
* unreadable database, a secret missing from the process environment. These
|
|
61
|
+
* are what make a reachable app `live_with_issues`; unfinished setup is not.
|
|
62
|
+
*/
|
|
63
|
+
reportedIssues?: StepProblem[];
|
|
29
64
|
syncInFlight: boolean;
|
|
30
65
|
/** A trigger URL and secret both exist, so Clivly can ask the app to sync. */
|
|
31
66
|
triggerConfigured: boolean;
|
|
@@ -46,6 +81,52 @@ interface IntegrationStep {
|
|
|
46
81
|
state: StepState;
|
|
47
82
|
}
|
|
48
83
|
declare function formatAge(ms: number): string;
|
|
84
|
+
/** The cadence `createClivlySDK` uses when the host app configures none. */
|
|
85
|
+
declare const DEFAULT_HEARTBEAT_INTERVAL_MS = 60000;
|
|
86
|
+
type ConnectionHealth = "live" | "live_with_issues" | "stale" | "offline";
|
|
87
|
+
/** Which algorithm decides whether this deployment is up. */
|
|
88
|
+
type PresenceTransport = "websocket" | "http";
|
|
89
|
+
type PresenceConnectionState = "connected" | "reconnecting" | "disconnected";
|
|
90
|
+
/**
|
|
91
|
+
* A deployment's connection state as its `PresenceRoom` last persisted it.
|
|
92
|
+
*
|
|
93
|
+
* This exists because a socket has an explicit state and a timestamp does not.
|
|
94
|
+
* Deriving WebSocket liveness from heartbeat age would leave a disconnected app
|
|
95
|
+
* shown as live for 150 seconds and offline only after ten minutes, which is
|
|
96
|
+
* the whole reason this transport exists.
|
|
97
|
+
*/
|
|
98
|
+
interface DeploymentPresence {
|
|
99
|
+
/** Sockets currently accepted in the room. */
|
|
100
|
+
activeInstanceCount: number;
|
|
101
|
+
connectionState: PresenceConnectionState;
|
|
102
|
+
transport: PresenceTransport;
|
|
103
|
+
}
|
|
104
|
+
interface ConnectionLiveness {
|
|
105
|
+
/** `"8s ago"`, or `"No heartbeat yet"` when the app has never reported. */
|
|
106
|
+
ageLabel: string;
|
|
107
|
+
ageMs: number | null;
|
|
108
|
+
environment: HostEnvironment | null;
|
|
109
|
+
health: ConnectionHealth;
|
|
110
|
+
/**
|
|
111
|
+
* Faults on a reachable app. Empty unless `health` is `live_with_issues`.
|
|
112
|
+
*/
|
|
113
|
+
issues: StepProblem[];
|
|
114
|
+
/** Present when a deployment-presence record supplied this liveness result. */
|
|
115
|
+
presence?: Pick<DeploymentPresence, "connectionState" | "transport">;
|
|
116
|
+
/** Is a heartbeat recent enough to speak for the app as it is right now? */
|
|
117
|
+
reachable: boolean;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The two boundaries that separate live from stale from offline, in terms of
|
|
121
|
+
* the app's own reported cadence. A cadence we cannot believe — absent, zero,
|
|
122
|
+
* negative — falls back to the SDK default rather than producing a window of
|
|
123
|
+
* zero that would call every app offline.
|
|
124
|
+
*/
|
|
125
|
+
declare function livenessWindows(intervalMs: number | null | undefined): {
|
|
126
|
+
liveMs: number;
|
|
127
|
+
staleMs: number;
|
|
128
|
+
};
|
|
129
|
+
declare function deriveLiveness(facts: IntegrationFacts): ConnectionLiveness;
|
|
49
130
|
/**
|
|
50
131
|
* Walk the ladder once. A step is `blocked` when an earlier step it needs has
|
|
51
132
|
* not been satisfied — a prerequisite statement, not a verdict about this step
|
|
@@ -54,10 +135,9 @@ declare function formatAge(ms: number): string;
|
|
|
54
135
|
*/
|
|
55
136
|
declare function resolveSteps(facts: IntegrationFacts): IntegrationStep[];
|
|
56
137
|
/**
|
|
57
|
-
* Collapse the ladder to the single status the surfaces switch on.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* than how far setup once got.
|
|
138
|
+
* Collapse the ladder to the single status the surfaces switch on. This is the
|
|
139
|
+
* setup dimension only: it answers how far configuration got, never whether the
|
|
140
|
+
* app is up. Pair it with `deriveLiveness` for the second half of the picture.
|
|
61
141
|
*/
|
|
62
142
|
declare function deriveStatus(steps: IntegrationStep[], facts: IntegrationFacts): IntegrationStatus;
|
|
63
143
|
interface NextAction {
|
|
@@ -66,6 +146,13 @@ interface NextAction {
|
|
|
66
146
|
label: string;
|
|
67
147
|
}
|
|
68
148
|
interface IntegrationState {
|
|
149
|
+
/**
|
|
150
|
+
* Is the app running right now. Deliberately alongside `status` rather than
|
|
151
|
+
* folded into it: a surface must be able to say "setup reached mapping" and
|
|
152
|
+
* "the app is offline" in the same breath without either overwriting the
|
|
153
|
+
* other.
|
|
154
|
+
*/
|
|
155
|
+
liveness: ConnectionLiveness;
|
|
69
156
|
next: NextAction | null;
|
|
70
157
|
status: IntegrationStatus;
|
|
71
158
|
steps: IntegrationStep[];
|
|
@@ -82,7 +169,7 @@ declare const INSTALL_COMMAND = "npx clivly init";
|
|
|
82
169
|
* as the fix for a write problem (C-05). If you need a command for the
|
|
83
170
|
* next-action slot, call this — do not write another one.
|
|
84
171
|
*/
|
|
85
|
-
declare function nextAction(steps: IntegrationStep[], status: IntegrationStatus): NextAction | null;
|
|
172
|
+
declare function nextAction(steps: IntegrationStep[], status: IntegrationStatus, liveness: ConnectionLiveness): NextAction | null;
|
|
86
173
|
declare function computeIntegrationState(facts: IntegrationFacts): IntegrationState;
|
|
87
174
|
//#endregion
|
|
88
|
-
export { INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, IntegrationFacts, IntegrationState, IntegrationStatus, IntegrationStep, NextAction, RunFact, STEP_PREREQUISITES, StepId, StepProblem, StepState, computeIntegrationState, deriveStatus, formatAge, nextAction, resolveSteps, stepIndex };
|
|
175
|
+
export { ConnectionHealth, ConnectionLiveness, DEFAULT_HEARTBEAT_INTERVAL_MS, DeploymentPresence, HostEnvironment, INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, IntegrationFacts, IntegrationState, IntegrationStatus, IntegrationStep, NextAction, PresenceConnectionState, PresenceTransport, RunFact, STEP_PREREQUISITES, StepId, StepProblem, StepState, computeIntegrationState, deriveLiveness, deriveStatus, formatAge, livenessWindows, nextAction, resolveSteps, stepIndex };
|
|
@@ -30,8 +30,6 @@ const STEP_PREREQUISITES = {
|
|
|
30
30
|
function stepIndex(id) {
|
|
31
31
|
return INTEGRATION_STEPS.indexOf(id);
|
|
32
32
|
}
|
|
33
|
-
/** A heartbeat older than this means the app stopped checking in. */
|
|
34
|
-
const HEARTBEAT_STALE_MS = 600 * 1e3;
|
|
35
33
|
function formatAge(ms) {
|
|
36
34
|
if (ms < 6e4) return `${Math.max(1, Math.round(ms / 1e3))}s ago`;
|
|
37
35
|
if (ms < 36e5) return `${Math.round(ms / 6e4)}m ago`;
|
|
@@ -40,12 +38,96 @@ function formatAge(ms) {
|
|
|
40
38
|
function heartbeatAge(facts) {
|
|
41
39
|
return facts.lastHeartbeatAt === null ? null : facts.now - facts.lastHeartbeatAt;
|
|
42
40
|
}
|
|
41
|
+
/** The cadence `createClivlySDK` uses when the host app configures none. */
|
|
42
|
+
const DEFAULT_HEARTBEAT_INTERVAL_MS = 6e4;
|
|
43
|
+
const LIVE_INTERVALS = 2.5;
|
|
44
|
+
const STALE_INTERVALS = 10;
|
|
45
|
+
/**
|
|
46
|
+
* The two boundaries that separate live from stale from offline, in terms of
|
|
47
|
+
* the app's own reported cadence. A cadence we cannot believe — absent, zero,
|
|
48
|
+
* negative — falls back to the SDK default rather than producing a window of
|
|
49
|
+
* zero that would call every app offline.
|
|
50
|
+
*/
|
|
51
|
+
function livenessWindows(intervalMs) {
|
|
52
|
+
const interval = typeof intervalMs === "number" && intervalMs > 0 ? intervalMs : DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
53
|
+
return {
|
|
54
|
+
liveMs: interval * LIVE_INTERVALS,
|
|
55
|
+
staleMs: interval * STALE_INTERVALS
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Faults worth showing on an app that is up. Deliberately excludes anything the
|
|
60
|
+
* developer simply has not done yet — no schema, no mapping, no trigger are all
|
|
61
|
+
* setup stages, and painting a fresh install amber would make the signal
|
|
62
|
+
* meaningless.
|
|
63
|
+
*/
|
|
64
|
+
function livenessIssues(facts) {
|
|
65
|
+
const issues = [...facts.reportedIssues ?? []];
|
|
66
|
+
if (facts.lastRun && !facts.lastRun.ok) issues.push({
|
|
67
|
+
code: "sync_failed",
|
|
68
|
+
message: facts.lastRun.message ?? "The last sync run failed.",
|
|
69
|
+
fix: "Open the run log to see how your app responded."
|
|
70
|
+
});
|
|
71
|
+
return issues;
|
|
72
|
+
}
|
|
73
|
+
function websocketLiveness(facts, presence, base) {
|
|
74
|
+
if (!(presence.connectionState === "connected" && presence.activeInstanceCount > 0)) {
|
|
75
|
+
const health = presence.connectionState === "reconnecting" ? "stale" : "offline";
|
|
76
|
+
return {
|
|
77
|
+
...base,
|
|
78
|
+
health,
|
|
79
|
+
issues: [],
|
|
80
|
+
reachable: false
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const issues = livenessIssues(facts);
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
health: issues.length > 0 ? "live_with_issues" : "live",
|
|
87
|
+
issues,
|
|
88
|
+
reachable: true
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function deriveLiveness(facts) {
|
|
92
|
+
const ageMs = heartbeatAge(facts);
|
|
93
|
+
const environment = facts.environment ?? null;
|
|
94
|
+
const { liveMs, staleMs } = livenessWindows(facts.heartbeatIntervalMs);
|
|
95
|
+
const base = {
|
|
96
|
+
ageMs,
|
|
97
|
+
ageLabel: ageMs === null ? "No heartbeat yet" : formatAge(ageMs),
|
|
98
|
+
environment,
|
|
99
|
+
...facts.presence ? { presence: {
|
|
100
|
+
connectionState: facts.presence.connectionState,
|
|
101
|
+
transport: facts.presence.transport
|
|
102
|
+
} } : {}
|
|
103
|
+
};
|
|
104
|
+
if (facts.presence && facts.presence.transport === "websocket") return websocketLiveness(facts, facts.presence, base);
|
|
105
|
+
if (ageMs === null || ageMs >= staleMs) return {
|
|
106
|
+
...base,
|
|
107
|
+
health: "offline",
|
|
108
|
+
issues: [],
|
|
109
|
+
reachable: false
|
|
110
|
+
};
|
|
111
|
+
if (ageMs >= liveMs) return {
|
|
112
|
+
...base,
|
|
113
|
+
health: "stale",
|
|
114
|
+
issues: [],
|
|
115
|
+
reachable: false
|
|
116
|
+
};
|
|
117
|
+
const issues = livenessIssues(facts);
|
|
118
|
+
return {
|
|
119
|
+
...base,
|
|
120
|
+
health: issues.length > 0 ? "live_with_issues" : "live",
|
|
121
|
+
issues,
|
|
122
|
+
reachable: true
|
|
123
|
+
};
|
|
124
|
+
}
|
|
43
125
|
/** Is the raw condition behind each step met, ignoring prerequisites? */
|
|
44
126
|
function isSatisfied(id, facts) {
|
|
45
127
|
const age = heartbeatAge(facts);
|
|
46
128
|
switch (id) {
|
|
47
129
|
case "credential": return facts.hasCredential;
|
|
48
|
-
case "application": return age !== null
|
|
130
|
+
case "application": return facts.everConnected || age !== null;
|
|
49
131
|
case "schema": return facts.discoveredTableCount > 0;
|
|
50
132
|
case "mapping": return facts.mappedEntityCount > 0;
|
|
51
133
|
case "trigger": return facts.triggerConfigured;
|
|
@@ -68,19 +150,6 @@ function problemFor(id, facts) {
|
|
|
68
150
|
message: facts.lastRun.message ?? "The last sync run failed.",
|
|
69
151
|
fix: "Open the run log to see the host response."
|
|
70
152
|
};
|
|
71
|
-
if (id === "application") {
|
|
72
|
-
const age = heartbeatAge(facts);
|
|
73
|
-
if (age !== null && age >= HEARTBEAT_STALE_MS) return {
|
|
74
|
-
code: "heartbeat_stale",
|
|
75
|
-
message: `Your app stopped checking in ${formatAge(age)}.`,
|
|
76
|
-
fix: "Start your app, then run `npx clivly status`."
|
|
77
|
-
};
|
|
78
|
-
if (age === null && facts.everConnected) return {
|
|
79
|
-
code: "heartbeat_stale",
|
|
80
|
-
message: "Your application stopped checking in.",
|
|
81
|
-
fix: "Start your app, then run `npx clivly status`."
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
153
|
return null;
|
|
85
154
|
}
|
|
86
155
|
/**
|
|
@@ -146,14 +215,12 @@ const STEP_STATUS_WHEN_ACTIVE = {
|
|
|
146
215
|
widget: "healthy"
|
|
147
216
|
};
|
|
148
217
|
/**
|
|
149
|
-
* Collapse the ladder to the single status the surfaces switch on.
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* than how far setup once got.
|
|
218
|
+
* Collapse the ladder to the single status the surfaces switch on. This is the
|
|
219
|
+
* setup dimension only: it answers how far configuration got, never whether the
|
|
220
|
+
* app is up. Pair it with `deriveLiveness` for the second half of the picture.
|
|
153
221
|
*/
|
|
154
222
|
function deriveStatus(steps, facts) {
|
|
155
223
|
if (facts.syncInFlight) return "syncing";
|
|
156
|
-
if (steps.find((step) => step.id === "application")?.problem?.code === "heartbeat_stale") return "disconnected";
|
|
157
224
|
if (steps.some((step) => step.state === "failed")) return "error";
|
|
158
225
|
const blocking = steps.find((step) => step.state === "active" || step.state === "blocked");
|
|
159
226
|
if (blocking) return STEP_STATUS_WHEN_ACTIVE[blocking.id];
|
|
@@ -208,13 +275,13 @@ const NEXT_BY_STEP = {
|
|
|
208
275
|
* as the fix for a write problem (C-05). If you need a command for the
|
|
209
276
|
* next-action slot, call this — do not write another one.
|
|
210
277
|
*/
|
|
211
|
-
function nextAction(steps, status) {
|
|
278
|
+
function nextAction(steps, status, liveness) {
|
|
212
279
|
if (status === "healthy" || status === "syncing") return null;
|
|
213
280
|
const blocking = steps.find((step) => step.state === "failed") ?? steps.find((step) => step.state === "active");
|
|
214
281
|
if (!blocking) return null;
|
|
215
|
-
if (
|
|
216
|
-
label: "
|
|
217
|
-
command:
|
|
282
|
+
if (steps.find((step) => step.id === "application")?.state === "done" && !liveness.reachable) return {
|
|
283
|
+
label: "Start your app",
|
|
284
|
+
command: null,
|
|
218
285
|
envFile: null
|
|
219
286
|
};
|
|
220
287
|
return NEXT_BY_STEP[blocking.id];
|
|
@@ -222,11 +289,13 @@ function nextAction(steps, status) {
|
|
|
222
289
|
function computeIntegrationState(facts) {
|
|
223
290
|
const steps = resolveSteps(facts);
|
|
224
291
|
const status = deriveStatus(steps, facts);
|
|
292
|
+
const liveness = deriveLiveness(facts);
|
|
225
293
|
return {
|
|
226
294
|
status,
|
|
227
295
|
steps,
|
|
228
|
-
|
|
296
|
+
liveness,
|
|
297
|
+
next: nextAction(steps, status, liveness)
|
|
229
298
|
};
|
|
230
299
|
}
|
|
231
300
|
//#endregion
|
|
232
|
-
export { INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, STEP_PREREQUISITES, computeIntegrationState, deriveStatus, formatAge, nextAction, resolveSteps, stepIndex };
|
|
301
|
+
export { DEFAULT_HEARTBEAT_INTERVAL_MS, INSTALL_COMMAND, INTEGRATION_STEPS, INTEGRATION_STEP_LABELS, STEP_PREREQUISITES, computeIntegrationState, deriveLiveness, deriveStatus, formatAge, livenessWindows, nextAction, resolveSteps, stepIndex };
|
package/dist/mapping-form.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { b as
|
|
1
|
+
import { b as RelationshipVia, h as FilterExpr, x as RelationshipsMap } from "./entity-config-buOIVtMM.cjs";
|
|
2
2
|
//#region src/mapping-form.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* A relationship the mapping UI can offer or has configured, in the shape the
|
package/dist/mapping-form.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { b as
|
|
1
|
+
import { b as RelationshipVia, h as FilterExpr, x as RelationshipsMap } from "./entity-config-buOIVtMM.mjs";
|
|
2
2
|
//#region src/mapping-form.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* A relationship the mapping UI can offer or has configured, in the shape the
|
package/dist/mappings-config.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_entity_config = require("./entity-config-
|
|
2
|
+
const require_entity_config = require("./entity-config-Bfq3OkCU.cjs");
|
|
3
3
|
//#region src/mappings-config.ts
|
|
4
4
|
const RESERVED_FIELD = "id";
|
|
5
5
|
/**
|
|
@@ -122,5 +122,29 @@ function mappingsToEntitiesConfig(rows) {
|
|
|
122
122
|
for (const { row, key } of assigned) entities[key] = buildEntity(row, refIndex);
|
|
123
123
|
return { entities };
|
|
124
124
|
}
|
|
125
|
+
const CUSTOM_CONCEPT = "custom";
|
|
126
|
+
function entitiesToMappings(config) {
|
|
127
|
+
const rows = [];
|
|
128
|
+
for (const [entityKey, entity] of Object.entries(config.entities)) {
|
|
129
|
+
const row = {
|
|
130
|
+
entityKey,
|
|
131
|
+
sourceTable: entity.projection?.baseTable ?? entity.source,
|
|
132
|
+
targetConcept: entity.concept,
|
|
133
|
+
role: entity.role ?? DEFAULT_ROLE,
|
|
134
|
+
fieldMap: entity.projection ? {} : entity.fields
|
|
135
|
+
};
|
|
136
|
+
if (entity.filter) row.filter = entity.filter;
|
|
137
|
+
if (entity.relationships) row.relationships = entity.relationships;
|
|
138
|
+
if (entity.projection) row.projection = entity.projection;
|
|
139
|
+
if (entity.concept === CUSTOM_CONCEPT) {
|
|
140
|
+
row.objectTypeSlug = entityKey;
|
|
141
|
+
if (entity.objectType) row.objectType = entity.objectType;
|
|
142
|
+
}
|
|
143
|
+
if (entity.targetObjectTypeId) row.targetObjectTypeId = entity.targetObjectTypeId;
|
|
144
|
+
rows.push(row);
|
|
145
|
+
}
|
|
146
|
+
return rows;
|
|
147
|
+
}
|
|
125
148
|
//#endregion
|
|
149
|
+
exports.entitiesToMappings = entitiesToMappings;
|
|
126
150
|
exports.mappingsToEntitiesConfig = mappingsToEntitiesConfig;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { P as ProjectionMapping, a as ClivlyEntitiesConfig, h as FilterExpr, x as RelationshipsMap } from "./entity-config-buOIVtMM.cjs";
|
|
2
2
|
//#region src/mappings-config.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Mappings → Entity Config bridge.
|
|
@@ -43,5 +43,43 @@ interface EntityMappingRow {
|
|
|
43
43
|
* eligible — a no-op the view compiler and sync engine both tolerate.
|
|
44
44
|
*/
|
|
45
45
|
declare function mappingsToEntitiesConfig(rows: EntityMappingRow[]): ClivlyEntitiesConfig;
|
|
46
|
+
/**
|
|
47
|
+
* Entity Config → Mappings, the inverse of `mappingsToEntitiesConfig`.
|
|
48
|
+
*
|
|
49
|
+
* `objectType` in config creates the custom object but nothing creates the
|
|
50
|
+
* MAPPING, so config-as-code ended at a created object and a sync that stored
|
|
51
|
+
* nothing. This produces the rows that close that gap.
|
|
52
|
+
*
|
|
53
|
+
* The two shapes align column for column, with one asymmetry that cannot be
|
|
54
|
+
* resolved by naming: the row model has no entity-key column — a row is keyed by
|
|
55
|
+
* `(sourceTable, targetConcept, role)` — while a CUSTOM entity's key *is* its
|
|
56
|
+
* object type slug, and is routinely different from its source table
|
|
57
|
+
* ("client-accounts" derived from "accounts"). So the key travels explicitly on
|
|
58
|
+
* every row rather than being left for the reverse derivation to guess.
|
|
59
|
+
*/
|
|
60
|
+
interface MappingFromConfig {
|
|
61
|
+
/**
|
|
62
|
+
* The config entity key this row came from. Not a row column — the caller
|
|
63
|
+
* needs it to resolve a custom object type and to report what it wrote.
|
|
64
|
+
*/
|
|
65
|
+
entityKey: string;
|
|
66
|
+
fieldMap: Record<string, string>;
|
|
67
|
+
filter?: FilterExpr;
|
|
68
|
+
/** Declared custom object, when the config declares one rather than pinning an id. */
|
|
69
|
+
objectType?: {
|
|
70
|
+
iconKey?: string;
|
|
71
|
+
name: string;
|
|
72
|
+
pluralName?: string;
|
|
73
|
+
};
|
|
74
|
+
/** The custom object type's slug — the entity key. Custom entities only. */
|
|
75
|
+
objectTypeSlug?: string;
|
|
76
|
+
projection?: ProjectionMapping;
|
|
77
|
+
relationships?: RelationshipsMap;
|
|
78
|
+
role: string;
|
|
79
|
+
sourceTable: string;
|
|
80
|
+
targetConcept: string;
|
|
81
|
+
targetObjectTypeId?: string;
|
|
82
|
+
}
|
|
83
|
+
declare function entitiesToMappings(config: ClivlyEntitiesConfig): MappingFromConfig[];
|
|
46
84
|
//#endregion
|
|
47
|
-
export { EntityMappingRow, mappingsToEntitiesConfig };
|
|
85
|
+
export { EntityMappingRow, MappingFromConfig, entitiesToMappings, mappingsToEntitiesConfig };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { P as ProjectionMapping, a as ClivlyEntitiesConfig, h as FilterExpr, x as RelationshipsMap } from "./entity-config-buOIVtMM.mjs";
|
|
2
2
|
//#region src/mappings-config.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Mappings → Entity Config bridge.
|
|
@@ -43,5 +43,43 @@ interface EntityMappingRow {
|
|
|
43
43
|
* eligible — a no-op the view compiler and sync engine both tolerate.
|
|
44
44
|
*/
|
|
45
45
|
declare function mappingsToEntitiesConfig(rows: EntityMappingRow[]): ClivlyEntitiesConfig;
|
|
46
|
+
/**
|
|
47
|
+
* Entity Config → Mappings, the inverse of `mappingsToEntitiesConfig`.
|
|
48
|
+
*
|
|
49
|
+
* `objectType` in config creates the custom object but nothing creates the
|
|
50
|
+
* MAPPING, so config-as-code ended at a created object and a sync that stored
|
|
51
|
+
* nothing. This produces the rows that close that gap.
|
|
52
|
+
*
|
|
53
|
+
* The two shapes align column for column, with one asymmetry that cannot be
|
|
54
|
+
* resolved by naming: the row model has no entity-key column — a row is keyed by
|
|
55
|
+
* `(sourceTable, targetConcept, role)` — while a CUSTOM entity's key *is* its
|
|
56
|
+
* object type slug, and is routinely different from its source table
|
|
57
|
+
* ("client-accounts" derived from "accounts"). So the key travels explicitly on
|
|
58
|
+
* every row rather than being left for the reverse derivation to guess.
|
|
59
|
+
*/
|
|
60
|
+
interface MappingFromConfig {
|
|
61
|
+
/**
|
|
62
|
+
* The config entity key this row came from. Not a row column — the caller
|
|
63
|
+
* needs it to resolve a custom object type and to report what it wrote.
|
|
64
|
+
*/
|
|
65
|
+
entityKey: string;
|
|
66
|
+
fieldMap: Record<string, string>;
|
|
67
|
+
filter?: FilterExpr;
|
|
68
|
+
/** Declared custom object, when the config declares one rather than pinning an id. */
|
|
69
|
+
objectType?: {
|
|
70
|
+
iconKey?: string;
|
|
71
|
+
name: string;
|
|
72
|
+
pluralName?: string;
|
|
73
|
+
};
|
|
74
|
+
/** The custom object type's slug — the entity key. Custom entities only. */
|
|
75
|
+
objectTypeSlug?: string;
|
|
76
|
+
projection?: ProjectionMapping;
|
|
77
|
+
relationships?: RelationshipsMap;
|
|
78
|
+
role: string;
|
|
79
|
+
sourceTable: string;
|
|
80
|
+
targetConcept: string;
|
|
81
|
+
targetObjectTypeId?: string;
|
|
82
|
+
}
|
|
83
|
+
declare function entitiesToMappings(config: ClivlyEntitiesConfig): MappingFromConfig[];
|
|
46
84
|
//#endregion
|
|
47
|
-
export { EntityMappingRow, mappingsToEntitiesConfig };
|
|
85
|
+
export { EntityMappingRow, MappingFromConfig, entitiesToMappings, mappingsToEntitiesConfig };
|