@substrat-run/connector-planima 0.2.0 → 0.2.1
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/LICENSE +661 -0
- package/dist/api.d.ts +284 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +462 -0
- package/dist/api.js.map +1 -0
- package/dist/index.d.ts +429 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +646 -0
- package/dist/index.js.map +1 -0
- package/dist/mock.d.ts +122 -0
- package/dist/mock.d.ts.map +1 -0
- package/dist/mock.js +200 -0
- package/dist/mock.js.map +1 -0
- package/dist/plan.d.ts +141 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +226 -0
- package/dist/plan.js.map +1 -0
- package/package.json +13 -13
package/dist/api.js
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* A thin, typed client over the Planima REST API.
|
|
4
|
+
*
|
|
5
|
+
* Every call goes through the connection's `fetch`, never a global one: that is what
|
|
6
|
+
* gets it a timeout, an egress policy, and health recorded against the right
|
|
7
|
+
* connection. Module code cannot reach any of this — boundary-lint bans `fetch`
|
|
8
|
+
* outright — and a connector is host code.
|
|
9
|
+
*
|
|
10
|
+
* ## One host, and the two headers that are easy to get wrong
|
|
11
|
+
*
|
|
12
|
+
* Unlike Fortnox there is no separate OAuth origin: Planima has a single host and a
|
|
13
|
+
* static token, so there is nothing to mint and nothing to refresh. What replaces that
|
|
14
|
+
* complexity is two header details the docs state once and a client gets wrong
|
|
15
|
+
* silently:
|
|
16
|
+
*
|
|
17
|
+
* 1. **`Authorization` carries the bare token** — no `Bearer` prefix. Planima's own
|
|
18
|
+
* curl example is `-H "Authorization: NotARealToken+tm6rdPsx23u+4/HiguLIFQw="`.
|
|
19
|
+
* Prefixing it is a 401 that reads exactly like a bad token.
|
|
20
|
+
* 2. **`Accept: application/vnd.planima.v1+json`** pins the version. Omitting it works
|
|
21
|
+
* today — v1 is the default — and is precisely the kind of thing that breaks on the
|
|
22
|
+
* day the default moves, months after the code that omitted it was written. So it
|
|
23
|
+
* is sent on every request rather than left to the default.
|
|
24
|
+
*/
|
|
25
|
+
/** The one REST host. Everything hangs off this. */
|
|
26
|
+
export const PLANIMA_API_BASE = 'https://api.planima.se';
|
|
27
|
+
/** The version-pinning `Accept` value — sent on every request, never defaulted to. */
|
|
28
|
+
export const PLANIMA_ACCEPT = 'application/vnd.planima.v1+json';
|
|
29
|
+
/**
|
|
30
|
+
* Planima's page ceiling. Asking for more is not an error — the server silently caps —
|
|
31
|
+
* so the client asks for exactly this and paginates, rather than asking for a big
|
|
32
|
+
* number and believing the answer is complete.
|
|
33
|
+
*/
|
|
34
|
+
export const PLANIMA_MAX_PAGE = 50;
|
|
35
|
+
/**
|
|
36
|
+
* A Planima connection's credential — one static API token, and nothing else.
|
|
37
|
+
*
|
|
38
|
+
* Created by a person in Planima under *account settings → API*, and it carries
|
|
39
|
+
* **that person's access level**: a token minted by a read-only user cannot write, and
|
|
40
|
+
* one minted by an admin can do everything that admin can. That is a fact worth
|
|
41
|
+
* stating in a credential's docs because it is the whole security model — there are no
|
|
42
|
+
* scopes to narrow, so the narrowing is done by choosing which user mints the token.
|
|
43
|
+
* A connector that only reads should be handed a read-only user's token, and this one
|
|
44
|
+
* only reads.
|
|
45
|
+
*
|
|
46
|
+
* There is no refresh token and no expiry, which removes the rotation hazard entirely
|
|
47
|
+
* and replaces it with a different one: a token is valid until a human revokes it in
|
|
48
|
+
* Planima, so revocation is out-of-band and the first this connector hears of it is a
|
|
49
|
+
* 401. {@link PlanimaApiError.refused} is what carries that distinction to the health
|
|
50
|
+
* record.
|
|
51
|
+
*/
|
|
52
|
+
export const planimaSecret = z.object({
|
|
53
|
+
token: z.string().min(1),
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* A Planima API failure, with the two bits a caller actually branches on.
|
|
57
|
+
*
|
|
58
|
+
* `refused` means the provider said "not with this token" — a 401 or 403. Everything
|
|
59
|
+
* else (a timeout, a 5xx, a parse failure) says nothing about the credential, and
|
|
60
|
+
* treating it as a refusal would make a Planima outage look like every tenant's token
|
|
61
|
+
* going bad at once.
|
|
62
|
+
*
|
|
63
|
+
* `retryAfterSeconds` is set only on a 429 and only when Planima sent the header. It is
|
|
64
|
+
* the provider's own instruction, so the throttle below obeys it rather than guessing.
|
|
65
|
+
*/
|
|
66
|
+
export class PlanimaApiError extends Error {
|
|
67
|
+
status;
|
|
68
|
+
refused;
|
|
69
|
+
body;
|
|
70
|
+
retryAfterSeconds;
|
|
71
|
+
constructor(message, status, body, retryAfterSeconds = null) {
|
|
72
|
+
super(message);
|
|
73
|
+
this.name = 'PlanimaApiError';
|
|
74
|
+
this.status = status;
|
|
75
|
+
this.refused = status === 401 || status === 403;
|
|
76
|
+
this.body = body;
|
|
77
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Planima's error envelope. A caller wants the message from whichever shape arrived
|
|
82
|
+
* rather than a bare status.
|
|
83
|
+
*/
|
|
84
|
+
function errorMessage(body, status) {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(body);
|
|
87
|
+
const e = parsed;
|
|
88
|
+
if (typeof e.error === 'string')
|
|
89
|
+
return e.error;
|
|
90
|
+
if (typeof e.error === 'object' && e.error !== null && typeof e.error.message === 'string') {
|
|
91
|
+
return e.error.message;
|
|
92
|
+
}
|
|
93
|
+
if (typeof e.message === 'string')
|
|
94
|
+
return e.message;
|
|
95
|
+
if (Array.isArray(e.errors) && e.errors.length > 0) {
|
|
96
|
+
return e.errors.map((x) => (typeof x === 'string' ? x : JSON.stringify(x))).join('; ');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Not JSON — fall through to the raw slice, which is more use than nothing.
|
|
101
|
+
}
|
|
102
|
+
const slice = body.trim().slice(0, 200);
|
|
103
|
+
return slice === '' ? `HTTP ${status}` : slice;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* `JSON.parse`, but a non-JSON body stays inside this module's error contract.
|
|
107
|
+
*
|
|
108
|
+
* This runs AFTER `res.ok`, which is exactly when it bites: a proxy, captive portal or
|
|
109
|
+
* gateway that answers `200` with an HTML page makes a bare `JSON.parse` throw a
|
|
110
|
+
* `SyntaxError`, and the caller loses `status`, `body` and `refused` — the three fields
|
|
111
|
+
* {@link PlanimaApiError} exists to carry, and the ones a sweep reports and a probe
|
|
112
|
+
* branches on.
|
|
113
|
+
*/
|
|
114
|
+
function asJson(body, what) {
|
|
115
|
+
try {
|
|
116
|
+
return JSON.parse(body);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new PlanimaApiError(`${what} was not JSON`, 502, body);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Provider shapes. Extra fields are ignored; every field Planima documents as
|
|
124
|
+
// nullable is read as nullable, because `.optional()` alone is not enough.
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
/**
|
|
127
|
+
* A field Planima may send as an explicit `null`.
|
|
128
|
+
*
|
|
129
|
+
* `.optional()` permits an ABSENT key; Planima sends the key with `null` in it for
|
|
130
|
+
* anything unset — `address`, `zip_code`, `description` and most of `Action` are
|
|
131
|
+
* documented that way. Both mean "not set", so both become `null`, and a caller gets
|
|
132
|
+
* one case to handle instead of two.
|
|
133
|
+
*/
|
|
134
|
+
const nullableString = () => z
|
|
135
|
+
.union([z.string(), z.null()])
|
|
136
|
+
.optional()
|
|
137
|
+
.transform((v) => v ?? null);
|
|
138
|
+
const nullableNumber = () => z
|
|
139
|
+
.union([z.number(), z.null()])
|
|
140
|
+
.optional()
|
|
141
|
+
.transform((v) => v ?? null);
|
|
142
|
+
/** A list's pagination block — the cursor the walk below is driven by. */
|
|
143
|
+
export const planimaPagination = z.object({
|
|
144
|
+
total_count: z.number().int().nonnegative(),
|
|
145
|
+
offset: z.number().int().nonnegative(),
|
|
146
|
+
limit: z.number().int().positive(),
|
|
147
|
+
});
|
|
148
|
+
export const planimaOrganization = z.object({
|
|
149
|
+
id: z.number().int(),
|
|
150
|
+
name: z.string(),
|
|
151
|
+
updated_at: nullableString(),
|
|
152
|
+
created_at: nullableString(),
|
|
153
|
+
});
|
|
154
|
+
export const planimaFacility = z.object({
|
|
155
|
+
id: z.number().int(),
|
|
156
|
+
name: z.string(),
|
|
157
|
+
address: nullableString(),
|
|
158
|
+
zip_code: nullableString(),
|
|
159
|
+
region: nullableString(),
|
|
160
|
+
tags: z.array(z.string()).default([]),
|
|
161
|
+
/** Residential area (sv. BOA) in m². */
|
|
162
|
+
residential_area: nullableNumber(),
|
|
163
|
+
/** Non-residential area (sv. LOA) in m². */
|
|
164
|
+
non_residential_area: nullableNumber(),
|
|
165
|
+
year_of_construction: nullableNumber(),
|
|
166
|
+
description: nullableString(),
|
|
167
|
+
/**
|
|
168
|
+
* The owning organization, nested by Planima on a facility read.
|
|
169
|
+
*
|
|
170
|
+
* Nullable AND optional, which is not belt-and-braces: `.optional()` alone permits
|
|
171
|
+
* an absent key, and a provider that sends `"organization": null` — for a facility
|
|
172
|
+
* whose organization the token cannot see, say — fails the parse and takes the whole
|
|
173
|
+
* sweep down with it. That is the exact shape of the bug `connector-fortnox` shipped
|
|
174
|
+
* against a real company, and the mock reproduces it here rather than agreeing with
|
|
175
|
+
* a convenient assumption.
|
|
176
|
+
*/
|
|
177
|
+
organization: z
|
|
178
|
+
.union([planimaOrganization, z.null()])
|
|
179
|
+
.optional()
|
|
180
|
+
.transform((v) => v ?? null),
|
|
181
|
+
updated_at: nullableString(),
|
|
182
|
+
created_at: nullableString(),
|
|
183
|
+
});
|
|
184
|
+
export const planimaBuilding = z.object({
|
|
185
|
+
id: z.number().int(),
|
|
186
|
+
name: z.string(),
|
|
187
|
+
address: nullableString(),
|
|
188
|
+
zip_code: nullableString(),
|
|
189
|
+
region: nullableString(),
|
|
190
|
+
year_of_construction: nullableNumber(),
|
|
191
|
+
facility_id: z.number().int(),
|
|
192
|
+
updated_at: nullableString(),
|
|
193
|
+
created_at: nullableString(),
|
|
194
|
+
});
|
|
195
|
+
export const planimaComponent = z.object({
|
|
196
|
+
id: z.number().int(),
|
|
197
|
+
name: z.string(),
|
|
198
|
+
amount: nullableNumber(),
|
|
199
|
+
building_id: nullableNumber(),
|
|
200
|
+
/** The component DEFINITION's name — what kind of thing this is. */
|
|
201
|
+
component: nullableString(),
|
|
202
|
+
unit: nullableString(),
|
|
203
|
+
category: nullableString(),
|
|
204
|
+
/** `null` when the component has no specific type set. */
|
|
205
|
+
type: nullableString(),
|
|
206
|
+
facility_id: z.number().int(),
|
|
207
|
+
updated_at: nullableString(),
|
|
208
|
+
created_at: nullableString(),
|
|
209
|
+
});
|
|
210
|
+
/**
|
|
211
|
+
* One planned maintenance action — the row a maintenance plan is actually made of.
|
|
212
|
+
*
|
|
213
|
+
* Every price arrives as a JSON **number**. It does not stay one: see `plan.ts`, where
|
|
214
|
+
* it becomes a decimal string before it can reach a scope.
|
|
215
|
+
*/
|
|
216
|
+
export const planimaAction = z.object({
|
|
217
|
+
id: z.number().int(),
|
|
218
|
+
name: z.string(),
|
|
219
|
+
amount: nullableNumber(),
|
|
220
|
+
unit: nullableString(),
|
|
221
|
+
unit_price: nullableNumber(),
|
|
222
|
+
total_price: nullableNumber(),
|
|
223
|
+
total_price_incl_vat: nullableNumber(),
|
|
224
|
+
year: z.number().int(),
|
|
225
|
+
status: z.string(),
|
|
226
|
+
description: nullableString(),
|
|
227
|
+
/** Fraction of the cost treated as investment, as a decimal fraction. */
|
|
228
|
+
investment_rate: nullableNumber(),
|
|
229
|
+
/** VAT rate as a decimal fraction (0.25 = 25 %). */
|
|
230
|
+
vat_rate: nullableNumber(),
|
|
231
|
+
category: nullableString(),
|
|
232
|
+
location: nullableString(),
|
|
233
|
+
building: nullableString(),
|
|
234
|
+
building_id: nullableNumber(),
|
|
235
|
+
component_id: nullableNumber(),
|
|
236
|
+
is_energy_saving: z.boolean().optional().default(false),
|
|
237
|
+
co2_equivalent: nullableNumber(),
|
|
238
|
+
final_cost: nullableNumber(),
|
|
239
|
+
tags: z.array(z.string()).default([]),
|
|
240
|
+
facility: z.object({ id: z.number().int(), name: z.string() }).optional(),
|
|
241
|
+
/** Requires the Project feature; absent or null on accounts without it. */
|
|
242
|
+
project_id: nullableNumber(),
|
|
243
|
+
updated_at: nullableString(),
|
|
244
|
+
created_at: nullableString(),
|
|
245
|
+
});
|
|
246
|
+
/**
|
|
247
|
+
* The eight action statuses Planima documents as its `status` filter enum.
|
|
248
|
+
*
|
|
249
|
+
* Exported as data rather than enforced as a schema, and that is the point: the field
|
|
250
|
+
* itself is typed `string` in Planima's own spec, so a ninth status is an ordinary
|
|
251
|
+
* product change, not a protocol break. Parsing against a closed set would turn that
|
|
252
|
+
* into a sweep-wide throw — a whole tenant's plan failing to land because one action
|
|
253
|
+
* moved to a status added last week. A consumer that wants to branch on status has the
|
|
254
|
+
* list; the connector passes through whatever arrived.
|
|
255
|
+
*/
|
|
256
|
+
export const PLANIMA_ACTION_STATUSES = [
|
|
257
|
+
'draft',
|
|
258
|
+
'planned',
|
|
259
|
+
'prioritized',
|
|
260
|
+
'decided',
|
|
261
|
+
'in_progress',
|
|
262
|
+
'deferred',
|
|
263
|
+
'completed',
|
|
264
|
+
'inactive',
|
|
265
|
+
];
|
|
266
|
+
/**
|
|
267
|
+
* Planima's documented ceiling: 10 requests per 10 seconds per token (and 10,000 per
|
|
268
|
+
* 24 h, which a sweep of this shape cannot approach).
|
|
269
|
+
*
|
|
270
|
+
* The throttle below is a sliding window rather than a fixed delay because the two
|
|
271
|
+
* behave differently for the traffic this connector actually makes: a sweep is bursty —
|
|
272
|
+
* one facility is 3 requests back to back, then nothing while pages are landed. A fixed
|
|
273
|
+
* 1 s spacing would tax the quiet stretches for nothing; a window lets the burst
|
|
274
|
+
* through and only waits when the tenth request in ten seconds is genuinely due.
|
|
275
|
+
*/
|
|
276
|
+
const RATE_LIMIT_REQUESTS = 10;
|
|
277
|
+
const RATE_LIMIT_WINDOW_MS = 10_000;
|
|
278
|
+
export class PlanimaApi {
|
|
279
|
+
conn;
|
|
280
|
+
apiBase;
|
|
281
|
+
now;
|
|
282
|
+
sleep;
|
|
283
|
+
maxRateLimitRetries;
|
|
284
|
+
/**
|
|
285
|
+
* When the last {@link RATE_LIMIT_REQUESTS} requests went out, oldest first.
|
|
286
|
+
*
|
|
287
|
+
* Per-instance by default, matching `FortnoxApi`'s token cache: an instance is built
|
|
288
|
+
* for one unit of work, so the window lives exactly as long as that work and no
|
|
289
|
+
* cross-request state accumulates in a Worker's isolate. But the limit Planima
|
|
290
|
+
* enforces is per TOKEN, so a caller that builds several clients on one token passes
|
|
291
|
+
* {@link PlanimaApiOptions.rateWindow} to share one — which `sweepPlanimaPlan` does
|
|
292
|
+
* across every binding in a pass.
|
|
293
|
+
*
|
|
294
|
+
* What that still does not cover, stated rather than hidden: two sweeps overlapping
|
|
295
|
+
* on one token, or the tenant's own scripts using it. Both are absorbed by the 429
|
|
296
|
+
* retry below rather than prevented here, because preventing them needs a lock this
|
|
297
|
+
* connector has nowhere to keep.
|
|
298
|
+
*/
|
|
299
|
+
sent;
|
|
300
|
+
constructor(conn, options) {
|
|
301
|
+
this.conn = conn;
|
|
302
|
+
this.apiBase = options?.apiBase ?? PLANIMA_API_BASE;
|
|
303
|
+
this.now = options?.now ?? (() => Date.now());
|
|
304
|
+
this.sleep = options?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
305
|
+
this.maxRateLimitRetries = options?.maxRateLimitRetries ?? 3;
|
|
306
|
+
this.sent = options?.rateWindow ?? [];
|
|
307
|
+
}
|
|
308
|
+
secret() {
|
|
309
|
+
const parsed = planimaSecret.safeParse(this.conn.secret);
|
|
310
|
+
if (!parsed.success) {
|
|
311
|
+
throw new PlanimaApiError(`incomplete Planima credential: ${parsed.error.issues.map((i) => i.path.join('.')).join(', ')}`, 400, '');
|
|
312
|
+
}
|
|
313
|
+
return parsed.data;
|
|
314
|
+
}
|
|
315
|
+
/** Wait, if the sliding window says the next request would breach the limit. */
|
|
316
|
+
async throttle() {
|
|
317
|
+
const cutoff = this.now() - RATE_LIMIT_WINDOW_MS;
|
|
318
|
+
while (this.sent.length > 0 && this.sent[0] <= cutoff)
|
|
319
|
+
this.sent.shift();
|
|
320
|
+
if (this.sent.length >= RATE_LIMIT_REQUESTS) {
|
|
321
|
+
// The oldest request leaving the window is the earliest moment a new one fits.
|
|
322
|
+
const waitMs = this.sent[0] + RATE_LIMIT_WINDOW_MS - this.now();
|
|
323
|
+
if (waitMs > 0)
|
|
324
|
+
await this.sleep(waitMs);
|
|
325
|
+
const after = this.now() - RATE_LIMIT_WINDOW_MS;
|
|
326
|
+
while (this.sent.length > 0 && this.sent[0] <= after)
|
|
327
|
+
this.sent.shift();
|
|
328
|
+
}
|
|
329
|
+
this.sent.push(this.now());
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* A JSON GET against the REST host, throttled, and retried through a 429.
|
|
333
|
+
*
|
|
334
|
+
* The retry exists because the window above cannot be authoritative: it models one
|
|
335
|
+
* client's own traffic, and the tenant's token may be in use by their own scripts at
|
|
336
|
+
* the same time. When Planima says "too many", it says how long to wait — so the
|
|
337
|
+
* client obeys `Retry-After` rather than backing off on a schedule of its own
|
|
338
|
+
* invention, and gives up after {@link PlanimaApiOptions.maxRateLimitRetries} so a
|
|
339
|
+
* pathologically busy token surfaces as a failed sweep instead of a hung one.
|
|
340
|
+
*/
|
|
341
|
+
async getJson(path) {
|
|
342
|
+
const { token } = this.secret();
|
|
343
|
+
for (let attempt = 0;; attempt += 1) {
|
|
344
|
+
await this.throttle();
|
|
345
|
+
const res = await this.conn.fetch(`${this.apiBase}${path}`, {
|
|
346
|
+
headers: {
|
|
347
|
+
// Bare, no `Bearer` — see this module's header note.
|
|
348
|
+
Authorization: token,
|
|
349
|
+
Accept: PLANIMA_ACCEPT,
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
if (res.ok)
|
|
353
|
+
return asJson(await res.text(), `Planima GET ${path}`);
|
|
354
|
+
const body = await res.text();
|
|
355
|
+
if (res.status === 429 && attempt < this.maxRateLimitRetries) {
|
|
356
|
+
const retryAfter = retryAfterMs(res.headers?.get('Retry-After') ?? null);
|
|
357
|
+
await this.sleep(retryAfter);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
throw new PlanimaApiError(`Planima GET ${path} failed: ${errorMessage(body, res.status)}`, res.status, body, res.status === 429
|
|
361
|
+
? (retryAfterMs(res.headers?.get('Retry-After') ?? null) / 1000)
|
|
362
|
+
: null);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Walk every page of a list endpoint.
|
|
367
|
+
*
|
|
368
|
+
* Termination is driven by what came BACK, never by `total_count` alone: a plan that
|
|
369
|
+
* grows mid-walk would otherwise leave the loop reading past the end, and a
|
|
370
|
+
* `total_count` that disagrees with the rows (a filter applied server-side after the
|
|
371
|
+
* count) would loop forever. An empty page ends the walk, and `total_count` is used
|
|
372
|
+
* only as the belt to that braces — a bound on how many pages can be worth asking
|
|
373
|
+
* for.
|
|
374
|
+
*/
|
|
375
|
+
async list(path, schema, query = {}) {
|
|
376
|
+
const rows = [];
|
|
377
|
+
let offset = 0;
|
|
378
|
+
for (;;) {
|
|
379
|
+
const url = new URL(`${this.apiBase}${path}`);
|
|
380
|
+
for (const [k, v] of Object.entries(query)) {
|
|
381
|
+
if (v !== undefined)
|
|
382
|
+
url.searchParams.set(k, String(v));
|
|
383
|
+
}
|
|
384
|
+
url.searchParams.set('page[limit]', String(PLANIMA_MAX_PAGE));
|
|
385
|
+
url.searchParams.set('page[offset]', String(offset));
|
|
386
|
+
const requested = url.toString().slice(this.apiBase.length);
|
|
387
|
+
const raw = (await this.getJson(requested));
|
|
388
|
+
// A 200 with no `data` array is NOT an empty list, and the difference is the
|
|
389
|
+
// whole plan. `raw.data ?? []` would read a malformed success — a proxy's
|
|
390
|
+
// rewritten body, a partial outage, a shape change — as "this facility has no
|
|
391
|
+
// components any more", land it, and record the content hash for it. A consumer
|
|
392
|
+
// that swaps its plan on `final` then replaces real rows with nothing, and the
|
|
393
|
+
// next sweep sees an unchanged hash and never repairs it. So a missing `data` is
|
|
394
|
+
// a response fault, reported as one.
|
|
395
|
+
if (!Array.isArray(raw.data)) {
|
|
396
|
+
throw new PlanimaApiError(`Planima GET ${requested} returned success with no 'data' array`, 502, JSON.stringify(raw).slice(0, 500));
|
|
397
|
+
}
|
|
398
|
+
const page = z.array(schema).parse(raw.data);
|
|
399
|
+
rows.push(...page);
|
|
400
|
+
if (page.length === 0)
|
|
401
|
+
return rows;
|
|
402
|
+
const pagination = planimaPagination.safeParse(raw.pagination);
|
|
403
|
+
// No pagination block at all ⇒ the endpoint is not paged, and one page is the
|
|
404
|
+
// whole answer. Trusting a missing block as "there is more" would loop forever.
|
|
405
|
+
if (!pagination.success)
|
|
406
|
+
return rows;
|
|
407
|
+
offset += page.length;
|
|
408
|
+
if (offset >= pagination.data.total_count)
|
|
409
|
+
return rows;
|
|
410
|
+
// A page shorter than the limit with the count still ahead of us means the server
|
|
411
|
+
// has stopped handing rows over; believing the count would spin.
|
|
412
|
+
if (page.length < Math.min(PLANIMA_MAX_PAGE, pagination.data.limit))
|
|
413
|
+
return rows;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/** Every organization this token can see — the probe read, and the sweep's entry point. */
|
|
417
|
+
async organizations() {
|
|
418
|
+
return this.list('/organizations', planimaOrganization);
|
|
419
|
+
}
|
|
420
|
+
/** Every facility, optionally narrowed to one organization. */
|
|
421
|
+
async facilities(organizationId) {
|
|
422
|
+
return this.list('/facilities', planimaFacility, { organization_id: organizationId });
|
|
423
|
+
}
|
|
424
|
+
/** One facility's buildings. */
|
|
425
|
+
async buildings(facilityId) {
|
|
426
|
+
return this.list(`/facilities/${facilityId}/buildings`, planimaBuilding);
|
|
427
|
+
}
|
|
428
|
+
/** One facility's components. */
|
|
429
|
+
async components(facilityId) {
|
|
430
|
+
return this.list(`/facilities/${facilityId}/components`, planimaComponent);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* One facility's planned actions, over a year window.
|
|
434
|
+
*
|
|
435
|
+
* The window is server-side (`start_year`/`end_year`) rather than a filter applied
|
|
436
|
+
* after the fact, because a maintenance plan routinely runs 30 years out and pulling
|
|
437
|
+
* all of it to keep five years is a rate-limit budget spent on rows that get dropped.
|
|
438
|
+
*/
|
|
439
|
+
async actions(facilityId, window) {
|
|
440
|
+
return this.list('/actions', planimaAction, {
|
|
441
|
+
facility_id: facilityId,
|
|
442
|
+
start_year: window.fromYear,
|
|
443
|
+
end_year: window.toYear,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* `Retry-After` in milliseconds, defaulting to the full window.
|
|
449
|
+
*
|
|
450
|
+
* Planima documents it as a count of seconds. A missing or unparseable value falls back
|
|
451
|
+
* to the whole rate-limit window, which is the only wait guaranteed to clear a
|
|
452
|
+
* 10-per-10-seconds limit — a shorter guess just spends another request to be told the
|
|
453
|
+
* same thing.
|
|
454
|
+
*/
|
|
455
|
+
function retryAfterMs(header) {
|
|
456
|
+
const seconds = header === null ? Number.NaN : Number(header);
|
|
457
|
+
if (!Number.isFinite(seconds) || seconds < 0)
|
|
458
|
+
return RATE_LIMIT_WINDOW_MS;
|
|
459
|
+
// A provider that answers with an hour must not become an hour-long hung sweep.
|
|
460
|
+
return Math.min(seconds * 1000, RATE_LIMIT_WINDOW_MS * 3);
|
|
461
|
+
}
|
|
462
|
+
//# sourceMappingURL=api.js.map
|
package/dist/api.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|