@substrat-run/connector-planima 0.2.0 → 0.2.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/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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api.js","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}