@uk-energy/neso 0.1.0 → 0.1.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/CHANGELOG.md +5 -0
- package/LICENSE +21 -21
- package/dist/index.cjs +31 -139
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -137
- package/dist/index.d.ts +8 -137
- package/dist/index.js +13 -132
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# @uk-energy/neso
|
|
2
2
|
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
- The CKAN layer now lives in `@uk-energy/core` (`CkanClient`); `neso.ckan.*`, the helpers and
|
|
6
|
+
the `Ckan*` / `Datastore*` types are re-exported here, so nothing changes for callers.
|
|
7
|
+
|
|
3
8
|
## 0.1.0
|
|
4
9
|
|
|
5
10
|
- `NesoClient` for the NESO Data Portal (CKAN 2.8): `ckan.*` (`packageShow`, `packageSearch`,
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Joshua Serpis
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joshua Serpis
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
CHANGED
|
@@ -30,12 +30,10 @@ function financialYearInName(name) {
|
|
|
30
30
|
function resourceForFinancialYear(resources, startYear) {
|
|
31
31
|
return resources.find((r) => r.datastore_active && financialYearInName(r.name) === startYear);
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
// src/client.ts
|
|
35
33
|
var BASE_URL = "https://api.neso.energy/api/3/action";
|
|
36
34
|
var DEFAULT_RATE_LIMIT = { requestsPerSecond: 5, burst: 10 };
|
|
37
35
|
var DEFAULT_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
38
|
-
var MAX_PAGE_SIZE =
|
|
36
|
+
var MAX_PAGE_SIZE = core.CKAN_MAX_PAGE_SIZE;
|
|
39
37
|
var PACKAGES = {
|
|
40
38
|
thermalConstraintCosts: "thermal-constraint-costs",
|
|
41
39
|
constraintBreakdown: "constraint-breakdown",
|
|
@@ -45,45 +43,6 @@ var PACKAGES = {
|
|
|
45
43
|
dailyBalancingCosts: "daily-balancing-costs-balancing-services-use-of-system",
|
|
46
44
|
demandFlexibility: "demand-flexibility"
|
|
47
45
|
};
|
|
48
|
-
var DECIMAL = /^-?\d+(\.\d+)?([eE][-+]?\d+)?$/;
|
|
49
|
-
var NUMERIC_TYPES = /* @__PURE__ */ new Set([
|
|
50
|
-
"numeric",
|
|
51
|
-
"int4",
|
|
52
|
-
"int8",
|
|
53
|
-
"float8",
|
|
54
|
-
"float4",
|
|
55
|
-
"int",
|
|
56
|
-
"double precision"
|
|
57
|
-
]);
|
|
58
|
-
function normaliseRows(records, fields, extraNumeric = []) {
|
|
59
|
-
const numeric = new Set(extraNumeric);
|
|
60
|
-
for (const f of fields) if (NUMERIC_TYPES.has(f.type)) numeric.add(f.id);
|
|
61
|
-
return records.map((r) => {
|
|
62
|
-
const out = {};
|
|
63
|
-
for (const [k, v] of Object.entries(r)) {
|
|
64
|
-
if (k === "_full_text") continue;
|
|
65
|
-
if (numeric.has(k) && typeof v === "string" && DECIMAL.test(v.trim())) {
|
|
66
|
-
const n = Number(v);
|
|
67
|
-
out[k] = Number.isFinite(n) ? n : v;
|
|
68
|
-
} else {
|
|
69
|
-
out[k] = v;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return out;
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
function ident(name) {
|
|
76
|
-
return `"${name.replace(/"/g, '""')}"`;
|
|
77
|
-
}
|
|
78
|
-
function literal(value) {
|
|
79
|
-
return typeof value === "number" ? String(value) : `'${value.replace(/'/g, "''")}'`;
|
|
80
|
-
}
|
|
81
|
-
function rangeSql(resourceId, column, from, to, orderBy, opts = {}) {
|
|
82
|
-
const upper = opts.toExclusive ? "<" : "<=";
|
|
83
|
-
const extra = opts.where ? ` AND ${opts.where}` : "";
|
|
84
|
-
const order = orderBy.length ? ` ORDER BY ${orderBy.map(ident).join(", ")}` : "";
|
|
85
|
-
return `SELECT * FROM ${ident(resourceId)} WHERE ${ident(column)} >= ${literal(from)} AND ${ident(column)} ${upper} ${literal(to)}${extra}${order}`;
|
|
86
|
-
}
|
|
87
46
|
function localRange(from, to) {
|
|
88
47
|
const f = core.londonDate(from);
|
|
89
48
|
const t = core.londonDate(to);
|
|
@@ -97,8 +56,8 @@ function nextDay(date) {
|
|
|
97
56
|
}
|
|
98
57
|
var NesoClient = class {
|
|
99
58
|
http;
|
|
100
|
-
|
|
101
|
-
|
|
59
|
+
/** Raw CKAN API (`@uk-energy/core` `CkanClient`): `packageShow` (memoised), `datastoreSearch`, `datastoreAll`, `sql`… */
|
|
60
|
+
ckan;
|
|
102
61
|
constructor(opts = {}) {
|
|
103
62
|
this.http = new core.HttpClient({
|
|
104
63
|
baseUrl: BASE_URL,
|
|
@@ -106,91 +65,19 @@ var NesoClient = class {
|
|
|
106
65
|
cacheTtlMs: opts.cache ? DEFAULT_CACHE_TTL_MS : 0,
|
|
107
66
|
...opts
|
|
108
67
|
});
|
|
109
|
-
this.
|
|
68
|
+
this.ckan = new core.CkanClient(this.http, {
|
|
69
|
+
packageTtlMs: opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
|
|
70
|
+
maxPageSize: MAX_PAGE_SIZE
|
|
71
|
+
});
|
|
110
72
|
}
|
|
111
73
|
/** Any CKAN action; unwraps `{ success, result }` and throws `UkEnergyError` on `success: false`. */
|
|
112
74
|
async action(name, query) {
|
|
113
|
-
|
|
114
|
-
if (!res || typeof res !== "object" || !("success" in res)) {
|
|
115
|
-
throw new core.UkEnergyError(`Unexpected response shape for ${name}`);
|
|
116
|
-
}
|
|
117
|
-
if (!res.success) {
|
|
118
|
-
throw new core.UkEnergyError(`${name}: ${res.error?.message ?? JSON.stringify(res.error)}`);
|
|
119
|
-
}
|
|
120
|
-
return res.result;
|
|
75
|
+
return this.ckan.action(name, query);
|
|
121
76
|
}
|
|
122
|
-
/** Raw CKAN API. */
|
|
123
|
-
ckan = {
|
|
124
|
-
/** Dataset metadata and its resources (memoised for `cacheTtlMs`, default 10 minutes). */
|
|
125
|
-
packageShow: async (id) => {
|
|
126
|
-
const hit = this.packageCache.get(id);
|
|
127
|
-
if (hit && Date.now() - hit.at < this.packageTtlMs) return hit.value;
|
|
128
|
-
const value = this.action("package_show", { id }).then((pkg) => {
|
|
129
|
-
if (!pkg || !Array.isArray(pkg.resources)) {
|
|
130
|
-
throw new core.UkEnergyError(`package_show: unexpected shape for ${id}`);
|
|
131
|
-
}
|
|
132
|
-
return pkg;
|
|
133
|
-
});
|
|
134
|
-
this.packageCache.set(id, { at: Date.now(), value });
|
|
135
|
-
value.catch(() => this.packageCache.delete(id));
|
|
136
|
-
return value;
|
|
137
|
-
},
|
|
138
|
-
/** Search datasets by free text. */
|
|
139
|
-
packageSearch: async (q, rows = 20) => this.action("package_search", { q, rows }),
|
|
140
|
-
resourceShow: async (id) => this.action("resource_show", { id }),
|
|
141
|
-
/** One page of a datastore resource (up to 32,000 rows). Rows are normalised (see `normaliseRows`). */
|
|
142
|
-
datastoreSearch: async (resourceId, opts = {}) => {
|
|
143
|
-
const query = {
|
|
144
|
-
resource_id: resourceId,
|
|
145
|
-
limit: opts.limit ?? MAX_PAGE_SIZE,
|
|
146
|
-
sort: opts.sort ?? "_id"
|
|
147
|
-
};
|
|
148
|
-
if (opts.offset !== void 0) query.offset = opts.offset;
|
|
149
|
-
if (opts.filters) query.filters = JSON.stringify(opts.filters);
|
|
150
|
-
if (opts.fields) query.fields = opts.fields.join(",");
|
|
151
|
-
if (opts.q) query.q = opts.q;
|
|
152
|
-
const res = await this.action("datastore_search", query);
|
|
153
|
-
if (!Array.isArray(res?.records)) {
|
|
154
|
-
throw new core.UkEnergyError("Unexpected response shape for datastore_search");
|
|
155
|
-
}
|
|
156
|
-
return { ...res, records: normaliseRows(res.records, res.fields ?? []) };
|
|
157
|
-
},
|
|
158
|
-
/** Every row of a datastore resource, paging by `limit` (default 32,000). */
|
|
159
|
-
datastoreAll: async (resourceId, opts = {}) => {
|
|
160
|
-
const pageSize = Math.min(opts.limit ?? MAX_PAGE_SIZE, MAX_PAGE_SIZE);
|
|
161
|
-
const out = [];
|
|
162
|
-
for await (const page of core.pages({
|
|
163
|
-
fetchPage: (offset) => this.ckan.datastoreSearch(resourceId, {
|
|
164
|
-
...opts,
|
|
165
|
-
limit: pageSize,
|
|
166
|
-
offset: offset ?? 0
|
|
167
|
-
}),
|
|
168
|
-
items: (p) => p.records,
|
|
169
|
-
nextCursor: core.offsetCursor(pageSize, (p) => p.records.length)
|
|
170
|
-
})) {
|
|
171
|
-
out.push(...page.records);
|
|
172
|
-
}
|
|
173
|
-
return out;
|
|
174
|
-
},
|
|
175
|
-
/**
|
|
176
|
-
* `datastore_search_sql`: read-only PostgreSQL over the datastore. Resource ids are the
|
|
177
|
-
* table names (quote them). Numeric columns come back as strings from this endpoint; they
|
|
178
|
-
* are converted using the `fields` types, and `_full_text` is dropped.
|
|
179
|
-
*/
|
|
180
|
-
sql: async (sql, extraNumeric = []) => {
|
|
181
|
-
const res = await this.action("datastore_search_sql", {
|
|
182
|
-
sql
|
|
183
|
-
});
|
|
184
|
-
if (!Array.isArray(res?.records)) {
|
|
185
|
-
throw new core.UkEnergyError("Unexpected response shape for datastore_search_sql");
|
|
186
|
-
}
|
|
187
|
-
return normaliseRows(res.records, res.fields ?? [], extraNumeric);
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
77
|
/** Resolving resources inside datasets. */
|
|
191
78
|
resources = {
|
|
192
79
|
/** All datastore-backed resources of a dataset. */
|
|
193
|
-
datastore: async (packageId) =>
|
|
80
|
+
datastore: async (packageId) => this.ckan.datastoreResources(packageId),
|
|
194
81
|
/** The resource for a GB financial year (`2025` = April 2025 – March 2026), by name. */
|
|
195
82
|
forFinancialYear: async (packageId, startYear) => {
|
|
196
83
|
const pkg = await this.ckan.packageShow(packageId);
|
|
@@ -203,14 +90,7 @@ var NesoClient = class {
|
|
|
203
90
|
return r;
|
|
204
91
|
},
|
|
205
92
|
/** The datastore resource whose name matches exactly (or the regex), e.g. "DFS Utilisation Report". */
|
|
206
|
-
byName: async (packageId, name) =>
|
|
207
|
-
const list = await this.resources.datastore(packageId);
|
|
208
|
-
const r = list.find(
|
|
209
|
-
(x) => typeof name === "string" ? x.name.trim() === name.trim() : name.test(x.name)
|
|
210
|
-
);
|
|
211
|
-
if (!r) throw new core.UkEnergyError(`${packageId}: no datastore resource named ${String(name)}`);
|
|
212
|
-
return r;
|
|
213
|
-
}
|
|
93
|
+
byName: async (packageId, name) => this.ckan.resourceByName(packageId, name)
|
|
214
94
|
};
|
|
215
95
|
/** Rows of a per-financial-year dataset for an inclusive date range, across year boundaries. */
|
|
216
96
|
async byFinancialYear(packageId, column, range, orderBy, opts = {}) {
|
|
@@ -218,7 +98,7 @@ var NesoClient = class {
|
|
|
218
98
|
const out = [];
|
|
219
99
|
for (const fy of financialYearsBetween(from, to)) {
|
|
220
100
|
const resource = await this.resources.forFinancialYear(packageId, fy);
|
|
221
|
-
const sql = rangeSql(resource.id, column, from, to, orderBy, { where: opts.where });
|
|
101
|
+
const sql = core.rangeSql(resource.id, column, from, to, orderBy, { where: opts.where });
|
|
222
102
|
out.push(...await this.ckan.sql(sql, opts.extraNumeric));
|
|
223
103
|
}
|
|
224
104
|
return out;
|
|
@@ -235,7 +115,7 @@ var NesoClient = class {
|
|
|
235
115
|
"Settlement Date",
|
|
236
116
|
{ from, to },
|
|
237
117
|
["Settlement Date", "Constraint Group"],
|
|
238
|
-
constraintGroup ? { where: `${ident("Constraint Group")} = ${literal(constraintGroup)}` } : {}
|
|
118
|
+
constraintGroup ? { where: `${core.ident("Constraint Group")} = ${core.literal(constraintGroup)}` } : {}
|
|
239
119
|
),
|
|
240
120
|
/**
|
|
241
121
|
* Daily constraint cost (£) and volume (MWh) by type: thermal, voltage, inertia, largest
|
|
@@ -262,7 +142,7 @@ var NesoClient = class {
|
|
|
262
142
|
dayAheadFlows: async (from, to, constraintGroup) => {
|
|
263
143
|
const [f, t] = localRange(from, to);
|
|
264
144
|
const r = await this.resources.byName(PACKAGES.dayAheadConstraintFlows, /Flows and Limits/);
|
|
265
|
-
const sql = rangeSql(
|
|
145
|
+
const sql = core.rangeSql(
|
|
266
146
|
r.id,
|
|
267
147
|
"Date_ Time GMT_BST",
|
|
268
148
|
`${f}T00:00:00`,
|
|
@@ -270,7 +150,7 @@ var NesoClient = class {
|
|
|
270
150
|
["Constraint Group", "Date_ Time GMT_BST"],
|
|
271
151
|
{
|
|
272
152
|
toExclusive: true,
|
|
273
|
-
where: constraintGroup ? `${ident("Constraint Group")} = ${literal(constraintGroup)}` : void 0
|
|
153
|
+
where: constraintGroup ? `${core.ident("Constraint Group")} = ${core.literal(constraintGroup)}` : void 0
|
|
274
154
|
}
|
|
275
155
|
);
|
|
276
156
|
return this.ckan.sql(sql);
|
|
@@ -303,10 +183,26 @@ var NesoClient = class {
|
|
|
303
183
|
async dfsRange(name, from, to, orderBy) {
|
|
304
184
|
const [f, t] = localRange(from, to);
|
|
305
185
|
const r = await this.resources.byName(PACKAGES.demandFlexibility, name);
|
|
306
|
-
return this.ckan.sql(rangeSql(r.id, "Delivery Date", f, t, orderBy));
|
|
186
|
+
return this.ckan.sql(core.rangeSql(r.id, "Delivery Date", f, t, orderBy));
|
|
307
187
|
}
|
|
308
188
|
};
|
|
309
189
|
|
|
190
|
+
Object.defineProperty(exports, "ident", {
|
|
191
|
+
enumerable: true,
|
|
192
|
+
get: function () { return core.ident; }
|
|
193
|
+
});
|
|
194
|
+
Object.defineProperty(exports, "literal", {
|
|
195
|
+
enumerable: true,
|
|
196
|
+
get: function () { return core.literal; }
|
|
197
|
+
});
|
|
198
|
+
Object.defineProperty(exports, "normaliseRows", {
|
|
199
|
+
enumerable: true,
|
|
200
|
+
get: function () { return core.normaliseRows; }
|
|
201
|
+
});
|
|
202
|
+
Object.defineProperty(exports, "rangeSql", {
|
|
203
|
+
enumerable: true,
|
|
204
|
+
get: function () { return core.rangeSql; }
|
|
205
|
+
});
|
|
310
206
|
exports.BASE_URL = BASE_URL;
|
|
311
207
|
exports.DEFAULT_CACHE_TTL_MS = DEFAULT_CACHE_TTL_MS;
|
|
312
208
|
exports.DEFAULT_RATE_LIMIT = DEFAULT_RATE_LIMIT;
|
|
@@ -316,10 +212,6 @@ exports.PACKAGES = PACKAGES;
|
|
|
316
212
|
exports.financialYearInName = financialYearInName;
|
|
317
213
|
exports.financialYearOf = financialYearOf;
|
|
318
214
|
exports.financialYearsBetween = financialYearsBetween;
|
|
319
|
-
exports.ident = ident;
|
|
320
|
-
exports.literal = literal;
|
|
321
|
-
exports.normaliseRows = normaliseRows;
|
|
322
|
-
exports.rangeSql = rangeSql;
|
|
323
215
|
exports.resourceForFinancialYear = resourceForFinancialYear;
|
|
324
216
|
//# sourceMappingURL=index.cjs.map
|
|
325
217
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resources.ts","../src/client.ts"],"names":["londonDate","UkEnergyError","HttpClient","pages","offsetCursor"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Financial-year arithmetic and resource-name matching for datasets split one resource per\n * GB financial year (1 April – 31 March): \"Thermal Constraint Costs Data 25-26\",\n * \"Constraint Breakdown 2025-2026\", \"Daily Balancing Costs 2025-2026\".\n */\nimport type { CkanResource } from \"./types.js\";\n\n/** Financial year a date (\"YYYY-MM-DD\") falls in, as its starting calendar year. */\nexport function financialYearOf(date: string): number {\n const year = Number(date.slice(0, 4));\n const month = Number(date.slice(5, 7));\n return month >= 4 ? year : year - 1;\n}\n\n/** Starting years of every financial year touched by an inclusive date range. */\nexport function financialYearsBetween(from: string, to: string): number[] {\n const first = financialYearOf(from);\n const last = financialYearOf(to);\n const out: number[] = [];\n for (let y = first; y <= last; y++) out.push(y);\n return out;\n}\n\nconst YEAR_SPAN = /(?<!\\d)(\\d{4}|\\d{2})\\s*[-/–]\\s*(\\d{4}|\\d{2})(?!\\d)/g;\n\n/** The financial year a resource name refers to (\"25-26\" / \"2025-2026\" → 2025), or undefined. */\nexport function financialYearInName(name: string): number | undefined {\n for (const m of name.matchAll(YEAR_SPAN)) {\n if (!m[1] || !m[2]) continue;\n const start = m[1].length === 2 ? 2000 + Number(m[1]) : Number(m[1]);\n const end = m[2].length === 2 ? 2000 + Number(m[2]) : Number(m[2]);\n if (end === start + 1) return start;\n }\n return undefined;\n}\n\n/** The datastore resource of a dataset for one financial year, if any. */\nexport function resourceForFinancialYear(\n resources: readonly CkanResource[],\n startYear: number,\n): CkanResource | undefined {\n return resources.find((r) => r.datastore_active && financialYearInName(r.name) === startYear);\n}\n","/**\n * Client for the NESO Data Portal (https://www.neso.energy/data-portal), a CKAN 2.8 instance at\n * https://api.neso.energy/api/3/action/. No API key. Datasets (\"packages\") hold \"resources\";\n * tabular resources are loaded into a Postgres datastore that `datastore_search` and\n * `datastore_search_sql` query. Many datasets split one resource per GB financial year\n * (April–March), so the helpers resolve the right resource(s) for a date range.\n *\n * const neso = new NesoClient();\n * const costs = await neso.constraints.thermalCosts(\"2025-04-01\", \"2025-04-07\"); // £/day per boundary\n * const dfs = await neso.dfs.summary(\"2026-10-20\", \"2026-10-22\");\n * const rows = await neso.ckan.sql(`SELECT * FROM \"407f3e9d-…\" WHERE \"Settlement Date\" = '2025-04-01'`);\n *\n * Licence: National Energy SO Open Data Licence v1.0 (OGL v3 based): copy, adapt, use\n * commercially, with the attribution \"Supported by National Energy SO Open Data\".\n */\nimport {\n type DateInput,\n HttpClient,\n type HttpClientOptions,\n londonDate,\n offsetCursor,\n pages,\n type Query,\n type RateLimit,\n UkEnergyError,\n} from \"@uk-energy/core\";\nimport { financialYearsBetween, resourceForFinancialYear } from \"./resources.js\";\nimport type {\n CkanPackage,\n CkanPackageSearch,\n CkanResource,\n CkanResponse,\n ConstraintBreakdownRow,\n ConstraintForecastRow,\n ConstraintLimitRow,\n DailyBalancingCostRow,\n DatastoreField,\n DatastoreRow,\n DatastoreSearchOptions,\n DatastoreSearchResult,\n DatastoreSqlResult,\n DayAheadFlowRow,\n DfsRequirementRow,\n DfsSummaryRow,\n DfsUtilisationRow,\n ThermalConstraintCostRow,\n} from \"./types.js\";\n\nexport const BASE_URL = \"https://api.neso.energy/api/3/action\";\n\n/**\n * No limit is published; the licence only reserves the right to throttle \"excessive use\".\n * 5 rps / burst 10 is comfortably polite for a shared public CKAN.\n */\nexport const DEFAULT_RATE_LIMIT: RateLimit = { requestsPerSecond: 5, burst: 10 };\n\n/** Resources update daily or weekly; package metadata is memoised for this long. */\nexport const DEFAULT_CACHE_TTL_MS = 10 * 60 * 1000;\n\n/** The portal returns up to this many rows per `datastore_search` page. */\nexport const MAX_PAGE_SIZE = 32_000;\n\n/** Dataset (package) names on the portal that the helpers use. */\nexport const PACKAGES = {\n thermalConstraintCosts: \"thermal-constraint-costs\",\n constraintBreakdown: \"constraint-breakdown\",\n constraintCostForecast24m: \"24-months-ahead-constraint-cost-forecast\",\n constraintLimits24m: \"24-months-ahead-constraint-limits\",\n dayAheadConstraintFlows: \"day-ahead-constraint-flows-and-limits\",\n dailyBalancingCosts: \"daily-balancing-costs-balancing-services-use-of-system\",\n demandFlexibility: \"demand-flexibility\",\n} as const;\n\nexport interface NesoClientOptions extends Omit<HttpClientOptions, \"baseUrl\"> {\n baseUrl?: string;\n}\n\n/** Inclusive date range; strings are \"YYYY-MM-DD\", `Date`s are GB-local calendar days. */\ninterface DateRange {\n from: DateInput;\n to: DateInput;\n}\n\nconst DECIMAL = /^-?\\d+(\\.\\d+)?([eE][-+]?\\d+)?$/;\n\nconst NUMERIC_TYPES = new Set([\n \"numeric\",\n \"int4\",\n \"int8\",\n \"float8\",\n \"float4\",\n \"int\",\n \"double precision\",\n]);\n\n/**\n * The SQL endpoint returns `numeric` columns as strings (\"252\"); `datastore_search` returns\n * numbers. Make rows consistent: numeric-typed columns become numbers, `_full_text` goes.\n */\nexport function normaliseRows<Row extends object>(\n records: readonly DatastoreRow[],\n fields: readonly DatastoreField[],\n extraNumeric: readonly string[] = [],\n): Row[] {\n const numeric = new Set(extraNumeric);\n for (const f of fields) if (NUMERIC_TYPES.has(f.type)) numeric.add(f.id);\n return records.map((r) => {\n const out: DatastoreRow = {};\n for (const [k, v] of Object.entries(r)) {\n if (k === \"_full_text\") continue;\n if (numeric.has(k) && typeof v === \"string\" && DECIMAL.test(v.trim())) {\n const n = Number(v);\n out[k] = Number.isFinite(n) ? n : v;\n } else {\n out[k] = v;\n }\n }\n return out as Row;\n });\n}\n\n/** Double-quote an identifier for the SQL endpoint. */\nexport function ident(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/** Single-quote a literal for the SQL endpoint. */\nexport function literal(value: string | number): string {\n return typeof value === \"number\" ? String(value) : `'${value.replace(/'/g, \"''\")}'`;\n}\n\n/**\n * `SELECT * FROM \"resource\" WHERE \"col\" >= 'from' AND \"col\" <= 'to' ORDER BY …` — the shape\n * every date-range helper uses. `toExclusive` switches the upper bound to `<` for timestamps.\n */\nexport function rangeSql(\n resourceId: string,\n column: string,\n from: string,\n to: string,\n orderBy: readonly string[],\n opts: { toExclusive?: boolean; where?: string | undefined } = {},\n): string {\n const upper = opts.toExclusive ? \"<\" : \"<=\";\n const extra = opts.where ? ` AND ${opts.where}` : \"\";\n const order = orderBy.length ? ` ORDER BY ${orderBy.map(ident).join(\", \")}` : \"\";\n return `SELECT * FROM ${ident(resourceId)} WHERE ${ident(column)} >= ${literal(from)} AND ${ident(column)} ${upper} ${literal(to)}${extra}${order}`;\n}\n\n/** Inclusive GB-local date range: `Date`s are read as Europe/London calendar days. */\nfunction localRange(from: DateInput, to: DateInput): [string, string] {\n const f = londonDate(from);\n const t = londonDate(to);\n if (f > t) throw new UkEnergyError(`from (${f}) is after to (${t})`);\n return [f, t];\n}\n\nfunction nextDay(date: string): string {\n const d = new Date(`${date}T00:00:00Z`);\n d.setUTCDate(d.getUTCDate() + 1);\n return d.toISOString().slice(0, 10);\n}\n\nexport class NesoClient {\n readonly http: HttpClient;\n private readonly packageCache = new Map<string, { at: number; value: Promise<CkanPackage> }>();\n private readonly packageTtlMs: number;\n\n constructor(opts: NesoClientOptions = {}) {\n this.http = new HttpClient({\n baseUrl: BASE_URL,\n rateLimit: DEFAULT_RATE_LIMIT,\n cacheTtlMs: opts.cache ? DEFAULT_CACHE_TTL_MS : 0,\n ...opts,\n });\n this.packageTtlMs = opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;\n }\n\n /** Any CKAN action; unwraps `{ success, result }` and throws `UkEnergyError` on `success: false`. */\n async action<T>(name: string, query?: Query): Promise<T> {\n const res = await this.http.get<CkanResponse<T>>(name, query);\n if (!res || typeof res !== \"object\" || !(\"success\" in res)) {\n throw new UkEnergyError(`Unexpected response shape for ${name}`);\n }\n if (!res.success) {\n throw new UkEnergyError(`${name}: ${res.error?.message ?? JSON.stringify(res.error)}`);\n }\n return res.result;\n }\n\n /** Raw CKAN API. */\n readonly ckan = {\n /** Dataset metadata and its resources (memoised for `cacheTtlMs`, default 10 minutes). */\n packageShow: async (id: string): Promise<CkanPackage> => {\n const hit = this.packageCache.get(id);\n if (hit && Date.now() - hit.at < this.packageTtlMs) return hit.value;\n const value = this.action<CkanPackage>(\"package_show\", { id }).then((pkg) => {\n if (!pkg || !Array.isArray(pkg.resources)) {\n throw new UkEnergyError(`package_show: unexpected shape for ${id}`);\n }\n return pkg;\n });\n this.packageCache.set(id, { at: Date.now(), value });\n value.catch(() => this.packageCache.delete(id));\n return value;\n },\n /** Search datasets by free text. */\n packageSearch: async (q: string, rows = 20): Promise<CkanPackageSearch> =>\n this.action(\"package_search\", { q, rows }),\n resourceShow: async (id: string): Promise<CkanResource> => this.action(\"resource_show\", { id }),\n /** One page of a datastore resource (up to 32,000 rows). Rows are normalised (see `normaliseRows`). */\n datastoreSearch: async <Row extends object = DatastoreRow>(\n resourceId: string,\n opts: DatastoreSearchOptions = {},\n ): Promise<DatastoreSearchResult<Row>> => {\n const query: Query = {\n resource_id: resourceId,\n limit: opts.limit ?? MAX_PAGE_SIZE,\n sort: opts.sort ?? \"_id\",\n };\n if (opts.offset !== undefined) query.offset = opts.offset;\n if (opts.filters) query.filters = JSON.stringify(opts.filters);\n if (opts.fields) query.fields = opts.fields.join(\",\");\n if (opts.q) query.q = opts.q;\n const res = await this.action<DatastoreSearchResult<DatastoreRow>>(\"datastore_search\", query);\n if (!Array.isArray(res?.records)) {\n throw new UkEnergyError(\"Unexpected response shape for datastore_search\");\n }\n return { ...res, records: normaliseRows<Row>(res.records, res.fields ?? []) };\n },\n /** Every row of a datastore resource, paging by `limit` (default 32,000). */\n datastoreAll: async <Row extends object = DatastoreRow>(\n resourceId: string,\n opts: Omit<DatastoreSearchOptions, \"offset\"> = {},\n ): Promise<Row[]> => {\n const pageSize = Math.min(opts.limit ?? MAX_PAGE_SIZE, MAX_PAGE_SIZE);\n const out: Row[] = [];\n for await (const page of pages<DatastoreSearchResult<Row>, Row, number>({\n fetchPage: (offset) =>\n this.ckan.datastoreSearch<Row>(resourceId, {\n ...opts,\n limit: pageSize,\n offset: offset ?? 0,\n }),\n items: (p) => p.records,\n nextCursor: offsetCursor(pageSize, (p) => p.records.length),\n })) {\n out.push(...page.records);\n }\n return out;\n },\n /**\n * `datastore_search_sql`: read-only PostgreSQL over the datastore. Resource ids are the\n * table names (quote them). Numeric columns come back as strings from this endpoint; they\n * are converted using the `fields` types, and `_full_text` is dropped.\n */\n sql: async <Row extends object = DatastoreRow>(\n sql: string,\n extraNumeric: readonly string[] = [],\n ): Promise<Row[]> => {\n const res = await this.action<DatastoreSqlResult<DatastoreRow>>(\"datastore_search_sql\", {\n sql,\n });\n if (!Array.isArray(res?.records)) {\n throw new UkEnergyError(\"Unexpected response shape for datastore_search_sql\");\n }\n return normaliseRows<Row>(res.records, res.fields ?? [], extraNumeric);\n },\n };\n\n /** Resolving resources inside datasets. */\n readonly resources = {\n /** All datastore-backed resources of a dataset. */\n datastore: async (packageId: string): Promise<CkanResource[]> =>\n (await this.ckan.packageShow(packageId)).resources.filter((r) => r.datastore_active),\n /** The resource for a GB financial year (`2025` = April 2025 – March 2026), by name. */\n forFinancialYear: async (packageId: string, startYear: number): Promise<CkanResource> => {\n const pkg = await this.ckan.packageShow(packageId);\n const r = resourceForFinancialYear(pkg.resources, startYear);\n if (!r) {\n throw new UkEnergyError(\n `${packageId}: no datastore resource for financial year ${startYear}/${(startYear + 1) % 100}`,\n );\n }\n return r;\n },\n /** The datastore resource whose name matches exactly (or the regex), e.g. \"DFS Utilisation Report\". */\n byName: async (packageId: string, name: string | RegExp): Promise<CkanResource> => {\n const list = await this.resources.datastore(packageId);\n const r = list.find((x) =>\n typeof name === \"string\" ? x.name.trim() === name.trim() : name.test(x.name),\n );\n if (!r) throw new UkEnergyError(`${packageId}: no datastore resource named ${String(name)}`);\n return r;\n },\n };\n\n /** Rows of a per-financial-year dataset for an inclusive date range, across year boundaries. */\n private async byFinancialYear<Row extends object>(\n packageId: string,\n column: string,\n range: DateRange,\n orderBy: readonly string[],\n opts: { where?: string | undefined; extraNumeric?: readonly string[] } = {},\n ): Promise<Row[]> {\n const [from, to] = localRange(range.from, range.to);\n const out: Row[] = [];\n for (const fy of financialYearsBetween(from, to)) {\n const resource = await this.resources.forFinancialYear(packageId, fy);\n const sql = rangeSql(resource.id, column, from, to, orderBy, { where: opts.where });\n out.push(...(await this.ckan.sql<Row>(sql, opts.extraNumeric)));\n }\n return out;\n }\n\n /** Transmission constraint costs and forecasts. */\n readonly constraints = {\n /**\n * Thermal constraint cost per boundary per day (£), `thermal-constraint-costs`. One row per\n * constraint group per settlement date, e.g. SCOTEX, SSHARN, SEIMP (Scottish export\n * boundaries), ESTEX, SWALEX.\n */\n thermalCosts: async (\n from: DateInput,\n to: DateInput,\n constraintGroup?: string,\n ): Promise<ThermalConstraintCostRow[]> =>\n this.byFinancialYear(\n PACKAGES.thermalConstraintCosts,\n \"Settlement Date\",\n { from, to },\n [\"Settlement Date\", \"Constraint Group\"],\n constraintGroup\n ? { where: `${ident(\"Constraint Group\")} = ${literal(constraintGroup)}` }\n : {},\n ),\n /**\n * Daily constraint cost (£) and volume (MWh) by type: thermal, voltage, inertia, largest\n * loss (`constraint-breakdown`). Thermal volume is negative (energy turned down).\n */\n breakdown: async (from: DateInput, to: DateInput): Promise<ConstraintBreakdownRow[]> =>\n this.byFinancialYear(PACKAGES.constraintBreakdown, \"Date\", { from, to }, [\"Date\"], {\n // \"Thermal constraints cost\" is typed text in some years\n extraNumeric: [\"Thermal constraints cost\"],\n }),\n /** NESO's 24-month-ahead constraint cost forecast, £m per month (24 rows). */\n forecast24m: async (): Promise<ConstraintForecastRow[]> => {\n const r = await this.resources.byName(PACKAGES.constraintCostForecast24m, /Forecast/);\n return this.ckan.datastoreAll<ConstraintForecastRow>(r.id);\n },\n /** 24-month-ahead boundary limits (MW) per week; boundary columns vary by publication. */\n limits24m: async (): Promise<ConstraintLimitRow[]> => {\n const r = await this.resources.byName(PACKAGES.constraintLimits24m, /limits/i);\n return this.ckan.datastoreAll<ConstraintLimitRow>(r.id);\n },\n /**\n * Day-ahead forecast flow vs limit per boundary, half-hourly, for a range of local dates\n * (`day-ahead-constraint-flows-and-limits`, ~750k rows, so always filtered by SQL).\n */\n dayAheadFlows: async (\n from: DateInput,\n to: DateInput,\n constraintGroup?: string,\n ): Promise<DayAheadFlowRow[]> => {\n const [f, t] = localRange(from, to);\n const r = await this.resources.byName(PACKAGES.dayAheadConstraintFlows, /Flows and Limits/);\n const sql = rangeSql(\n r.id,\n \"Date_ Time GMT_BST\",\n `${f}T00:00:00`,\n `${nextDay(t)}T00:00:00`,\n [\"Constraint Group\", \"Date_ Time GMT_BST\"],\n {\n toExclusive: true,\n where: constraintGroup\n ? `${ident(\"Constraint Group\")} = ${literal(constraintGroup)}`\n : undefined,\n },\n );\n return this.ckan.sql<DayAheadFlowRow>(sql);\n },\n /** Daily balancing (BSUoS) costs per settlement period by category, incl. `Constraints` (£). */\n dailyBalancingCosts: async (from: DateInput, to: DateInput): Promise<DailyBalancingCostRow[]> =>\n this.byFinancialYear(PACKAGES.dailyBalancingCosts, \"SETT_DATE\", { from, to }, [\n \"SETT_DATE\",\n \"SETT_PERIOD\",\n ]),\n };\n\n /** Demand Flexibility Service (current season; archives are separate resources). */\n readonly dfs = {\n /** Published requirements per event window, by delivery date. */\n requirements: async (from: DateInput, to: DateInput): Promise<DfsRequirementRow[]> =>\n this.dfsRange<DfsRequirementRow>(\"DFS Service Requirement\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n ]),\n /** Accepted/rejected bids per participant unit per event window. */\n utilisation: async (from: DateInput, to: DateInput): Promise<DfsUtilisationRow[]> =>\n this.dfsRange<DfsUtilisationRow>(\"DFS Utilisation Report\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n \"DFS Unit ID\",\n ]),\n /** Procured MW, accepted cost and settled volume/cost per event window. */\n summary: async (from: DateInput, to: DateInput): Promise<DfsSummaryRow[]> =>\n this.dfsRange<DfsSummaryRow>(\"DFS Utilisation Report Summary\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n ]),\n };\n\n private async dfsRange<Row extends object>(\n name: string,\n from: DateInput,\n to: DateInput,\n orderBy: readonly string[],\n ): Promise<Row[]> {\n const [f, t] = localRange(from, to);\n const r = await this.resources.byName(PACKAGES.demandFlexibility, name);\n return this.ckan.sql<Row>(rangeSql(r.id, \"Delivery Date\", f, t, orderBy));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/resources.ts","../src/client.ts"],"names":["CKAN_MAX_PAGE_SIZE","londonDate","UkEnergyError","HttpClient","CkanClient","rangeSql","ident","literal"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Financial-year arithmetic and resource-name matching for datasets split one resource per\n * GB financial year (1 April – 31 March): \"Thermal Constraint Costs Data 25-26\",\n * \"Constraint Breakdown 2025-2026\", \"Daily Balancing Costs 2025-2026\".\n */\nimport type { CkanResource } from \"./types.js\";\n\n/** Financial year a date (\"YYYY-MM-DD\") falls in, as its starting calendar year. */\nexport function financialYearOf(date: string): number {\n const year = Number(date.slice(0, 4));\n const month = Number(date.slice(5, 7));\n return month >= 4 ? year : year - 1;\n}\n\n/** Starting years of every financial year touched by an inclusive date range. */\nexport function financialYearsBetween(from: string, to: string): number[] {\n const first = financialYearOf(from);\n const last = financialYearOf(to);\n const out: number[] = [];\n for (let y = first; y <= last; y++) out.push(y);\n return out;\n}\n\nconst YEAR_SPAN = /(?<!\\d)(\\d{4}|\\d{2})\\s*[-/–]\\s*(\\d{4}|\\d{2})(?!\\d)/g;\n\n/** The financial year a resource name refers to (\"25-26\" / \"2025-2026\" → 2025), or undefined. */\nexport function financialYearInName(name: string): number | undefined {\n for (const m of name.matchAll(YEAR_SPAN)) {\n if (!m[1] || !m[2]) continue;\n const start = m[1].length === 2 ? 2000 + Number(m[1]) : Number(m[1]);\n const end = m[2].length === 2 ? 2000 + Number(m[2]) : Number(m[2]);\n if (end === start + 1) return start;\n }\n return undefined;\n}\n\n/** The datastore resource of a dataset for one financial year, if any. */\nexport function resourceForFinancialYear(\n resources: readonly CkanResource[],\n startYear: number,\n): CkanResource | undefined {\n return resources.find((r) => r.datastore_active && financialYearInName(r.name) === startYear);\n}\n","/**\n * Client for the NESO Data Portal (https://www.neso.energy/data-portal), a CKAN 2.8 instance at\n * https://api.neso.energy/api/3/action/. No API key. Datasets (\"packages\") hold \"resources\";\n * tabular resources are loaded into a Postgres datastore that `datastore_search` and\n * `datastore_search_sql` query. Many datasets split one resource per GB financial year\n * (April–March), so the helpers resolve the right resource(s) for a date range.\n *\n * const neso = new NesoClient();\n * const costs = await neso.constraints.thermalCosts(\"2025-04-01\", \"2025-04-07\"); // £/day per boundary\n * const dfs = await neso.dfs.summary(\"2026-10-20\", \"2026-10-22\");\n * const rows = await neso.ckan.sql(`SELECT * FROM \"407f3e9d-…\" WHERE \"Settlement Date\" = '2025-04-01'`);\n *\n * Licence: National Energy SO Open Data Licence v1.0 (OGL v3 based): copy, adapt, use\n * commercially, with the attribution \"Supported by National Energy SO Open Data\".\n */\nimport {\n CKAN_MAX_PAGE_SIZE,\n CkanClient,\n type CkanResource,\n type DateInput,\n HttpClient,\n type HttpClientOptions,\n ident,\n literal,\n londonDate,\n type Query,\n type RateLimit,\n rangeSql,\n UkEnergyError,\n} from \"@uk-energy/core\";\nimport { financialYearsBetween, resourceForFinancialYear } from \"./resources.js\";\nimport type {\n ConstraintBreakdownRow,\n ConstraintForecastRow,\n ConstraintLimitRow,\n DailyBalancingCostRow,\n DayAheadFlowRow,\n DfsRequirementRow,\n DfsSummaryRow,\n DfsUtilisationRow,\n ThermalConstraintCostRow,\n} from \"./types.js\";\n\nexport const BASE_URL = \"https://api.neso.energy/api/3/action\";\n\n/**\n * No limit is published; the licence only reserves the right to throttle \"excessive use\".\n * 5 rps / burst 10 is comfortably polite for a shared public CKAN.\n */\nexport const DEFAULT_RATE_LIMIT: RateLimit = { requestsPerSecond: 5, burst: 10 };\n\n/** Resources update daily or weekly; package metadata is memoised for this long. */\nexport const DEFAULT_CACHE_TTL_MS = 10 * 60 * 1000;\n\n/** The portal returns up to this many rows per `datastore_search` page. */\nexport const MAX_PAGE_SIZE = CKAN_MAX_PAGE_SIZE;\n\n/** Dataset (package) names on the portal that the helpers use. */\nexport const PACKAGES = {\n thermalConstraintCosts: \"thermal-constraint-costs\",\n constraintBreakdown: \"constraint-breakdown\",\n constraintCostForecast24m: \"24-months-ahead-constraint-cost-forecast\",\n constraintLimits24m: \"24-months-ahead-constraint-limits\",\n dayAheadConstraintFlows: \"day-ahead-constraint-flows-and-limits\",\n dailyBalancingCosts: \"daily-balancing-costs-balancing-services-use-of-system\",\n demandFlexibility: \"demand-flexibility\",\n} as const;\n\nexport interface NesoClientOptions extends Omit<HttpClientOptions, \"baseUrl\"> {\n baseUrl?: string;\n}\n\n/** Inclusive date range; strings are \"YYYY-MM-DD\", `Date`s are GB-local calendar days. */\ninterface DateRange {\n from: DateInput;\n to: DateInput;\n}\n\nexport { ident, literal, normaliseRows, rangeSql } from \"@uk-energy/core\";\n\n/** Inclusive GB-local date range: `Date`s are read as Europe/London calendar days. */\nfunction localRange(from: DateInput, to: DateInput): [string, string] {\n const f = londonDate(from);\n const t = londonDate(to);\n if (f > t) throw new UkEnergyError(`from (${f}) is after to (${t})`);\n return [f, t];\n}\n\nfunction nextDay(date: string): string {\n const d = new Date(`${date}T00:00:00Z`);\n d.setUTCDate(d.getUTCDate() + 1);\n return d.toISOString().slice(0, 10);\n}\n\nexport class NesoClient {\n readonly http: HttpClient;\n /** Raw CKAN API (`@uk-energy/core` `CkanClient`): `packageShow` (memoised), `datastoreSearch`, `datastoreAll`, `sql`… */\n readonly ckan: CkanClient;\n\n constructor(opts: NesoClientOptions = {}) {\n this.http = new HttpClient({\n baseUrl: BASE_URL,\n rateLimit: DEFAULT_RATE_LIMIT,\n cacheTtlMs: opts.cache ? DEFAULT_CACHE_TTL_MS : 0,\n ...opts,\n });\n this.ckan = new CkanClient(this.http, {\n packageTtlMs: opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,\n maxPageSize: MAX_PAGE_SIZE,\n });\n }\n\n /** Any CKAN action; unwraps `{ success, result }` and throws `UkEnergyError` on `success: false`. */\n async action<T>(name: string, query?: Query): Promise<T> {\n return this.ckan.action<T>(name, query);\n }\n\n /** Resolving resources inside datasets. */\n readonly resources = {\n /** All datastore-backed resources of a dataset. */\n datastore: async (packageId: string): Promise<CkanResource[]> =>\n this.ckan.datastoreResources(packageId),\n /** The resource for a GB financial year (`2025` = April 2025 – March 2026), by name. */\n forFinancialYear: async (packageId: string, startYear: number): Promise<CkanResource> => {\n const pkg = await this.ckan.packageShow(packageId);\n const r = resourceForFinancialYear(pkg.resources, startYear);\n if (!r) {\n throw new UkEnergyError(\n `${packageId}: no datastore resource for financial year ${startYear}/${(startYear + 1) % 100}`,\n );\n }\n return r;\n },\n /** The datastore resource whose name matches exactly (or the regex), e.g. \"DFS Utilisation Report\". */\n byName: async (packageId: string, name: string | RegExp): Promise<CkanResource> =>\n this.ckan.resourceByName(packageId, name),\n };\n\n /** Rows of a per-financial-year dataset for an inclusive date range, across year boundaries. */\n private async byFinancialYear<Row extends object>(\n packageId: string,\n column: string,\n range: DateRange,\n orderBy: readonly string[],\n opts: { where?: string | undefined; extraNumeric?: readonly string[] } = {},\n ): Promise<Row[]> {\n const [from, to] = localRange(range.from, range.to);\n const out: Row[] = [];\n for (const fy of financialYearsBetween(from, to)) {\n const resource = await this.resources.forFinancialYear(packageId, fy);\n const sql = rangeSql(resource.id, column, from, to, orderBy, { where: opts.where });\n out.push(...(await this.ckan.sql<Row>(sql, opts.extraNumeric)));\n }\n return out;\n }\n\n /** Transmission constraint costs and forecasts. */\n readonly constraints = {\n /**\n * Thermal constraint cost per boundary per day (£), `thermal-constraint-costs`. One row per\n * constraint group per settlement date, e.g. SCOTEX, SSHARN, SEIMP (Scottish export\n * boundaries), ESTEX, SWALEX.\n */\n thermalCosts: async (\n from: DateInput,\n to: DateInput,\n constraintGroup?: string,\n ): Promise<ThermalConstraintCostRow[]> =>\n this.byFinancialYear(\n PACKAGES.thermalConstraintCosts,\n \"Settlement Date\",\n { from, to },\n [\"Settlement Date\", \"Constraint Group\"],\n constraintGroup\n ? { where: `${ident(\"Constraint Group\")} = ${literal(constraintGroup)}` }\n : {},\n ),\n /**\n * Daily constraint cost (£) and volume (MWh) by type: thermal, voltage, inertia, largest\n * loss (`constraint-breakdown`). Thermal volume is negative (energy turned down).\n */\n breakdown: async (from: DateInput, to: DateInput): Promise<ConstraintBreakdownRow[]> =>\n this.byFinancialYear(PACKAGES.constraintBreakdown, \"Date\", { from, to }, [\"Date\"], {\n // \"Thermal constraints cost\" is typed text in some years\n extraNumeric: [\"Thermal constraints cost\"],\n }),\n /** NESO's 24-month-ahead constraint cost forecast, £m per month (24 rows). */\n forecast24m: async (): Promise<ConstraintForecastRow[]> => {\n const r = await this.resources.byName(PACKAGES.constraintCostForecast24m, /Forecast/);\n return this.ckan.datastoreAll<ConstraintForecastRow>(r.id);\n },\n /** 24-month-ahead boundary limits (MW) per week; boundary columns vary by publication. */\n limits24m: async (): Promise<ConstraintLimitRow[]> => {\n const r = await this.resources.byName(PACKAGES.constraintLimits24m, /limits/i);\n return this.ckan.datastoreAll<ConstraintLimitRow>(r.id);\n },\n /**\n * Day-ahead forecast flow vs limit per boundary, half-hourly, for a range of local dates\n * (`day-ahead-constraint-flows-and-limits`, ~750k rows, so always filtered by SQL).\n */\n dayAheadFlows: async (\n from: DateInput,\n to: DateInput,\n constraintGroup?: string,\n ): Promise<DayAheadFlowRow[]> => {\n const [f, t] = localRange(from, to);\n const r = await this.resources.byName(PACKAGES.dayAheadConstraintFlows, /Flows and Limits/);\n const sql = rangeSql(\n r.id,\n \"Date_ Time GMT_BST\",\n `${f}T00:00:00`,\n `${nextDay(t)}T00:00:00`,\n [\"Constraint Group\", \"Date_ Time GMT_BST\"],\n {\n toExclusive: true,\n where: constraintGroup\n ? `${ident(\"Constraint Group\")} = ${literal(constraintGroup)}`\n : undefined,\n },\n );\n return this.ckan.sql<DayAheadFlowRow>(sql);\n },\n /** Daily balancing (BSUoS) costs per settlement period by category, incl. `Constraints` (£). */\n dailyBalancingCosts: async (from: DateInput, to: DateInput): Promise<DailyBalancingCostRow[]> =>\n this.byFinancialYear(PACKAGES.dailyBalancingCosts, \"SETT_DATE\", { from, to }, [\n \"SETT_DATE\",\n \"SETT_PERIOD\",\n ]),\n };\n\n /** Demand Flexibility Service (current season; archives are separate resources). */\n readonly dfs = {\n /** Published requirements per event window, by delivery date. */\n requirements: async (from: DateInput, to: DateInput): Promise<DfsRequirementRow[]> =>\n this.dfsRange<DfsRequirementRow>(\"DFS Service Requirement\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n ]),\n /** Accepted/rejected bids per participant unit per event window. */\n utilisation: async (from: DateInput, to: DateInput): Promise<DfsUtilisationRow[]> =>\n this.dfsRange<DfsUtilisationRow>(\"DFS Utilisation Report\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n \"DFS Unit ID\",\n ]),\n /** Procured MW, accepted cost and settled volume/cost per event window. */\n summary: async (from: DateInput, to: DateInput): Promise<DfsSummaryRow[]> =>\n this.dfsRange<DfsSummaryRow>(\"DFS Utilisation Report Summary\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n ]),\n };\n\n private async dfsRange<Row extends object>(\n name: string,\n from: DateInput,\n to: DateInput,\n orderBy: readonly string[],\n ): Promise<Row[]> {\n const [f, t] = localRange(from, to);\n const r = await this.resources.byName(PACKAGES.demandFlexibility, name);\n return this.ckan.sql<Row>(rangeSql(r.id, \"Delivery Date\", f, t, orderBy));\n }\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { RateLimit, HttpClient, HttpClientOptions, Query, DateInput } from '@uk-energy/core';
|
|
1
|
+
import { RateLimit, HttpClient, CkanClient, HttpClientOptions, Query, CkanResource, DateInput } from '@uk-energy/core';
|
|
2
|
+
export { CkanFailure, CkanPackage, CkanPackageSearch, CkanResource, CkanResponse, CkanSuccess, DatastoreField, DatastoreRow, DatastoreSearchOptions, DatastoreSearchResult, DatastoreSqlResult, ident, literal, normaliseRows, rangeSql } from '@uk-energy/core';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Shapes of the NESO Data Portal (CKAN 2.8) API and of the datasets this package wraps.
|
|
@@ -8,103 +9,7 @@ import { RateLimit, HttpClient, HttpClientOptions, Query, DateInput } from '@uk-
|
|
|
8
9
|
*
|
|
9
10
|
* Supported by National Energy SO Open Data.
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
-
help: string;
|
|
13
|
-
success: true;
|
|
14
|
-
result: T;
|
|
15
|
-
}
|
|
16
|
-
interface CkanFailure {
|
|
17
|
-
help: string;
|
|
18
|
-
success: false;
|
|
19
|
-
error: {
|
|
20
|
-
message?: string;
|
|
21
|
-
__type?: string;
|
|
22
|
-
[field: string]: unknown;
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
type CkanResponse<T> = CkanSuccess<T> | CkanFailure;
|
|
26
|
-
interface CkanResource {
|
|
27
|
-
id: string;
|
|
28
|
-
package_id: string;
|
|
29
|
-
name: string;
|
|
30
|
-
description: string;
|
|
31
|
-
format: string;
|
|
32
|
-
url: string;
|
|
33
|
-
/** True when the resource is loaded into the datastore (CSV/XLSX pushed to Postgres). */
|
|
34
|
-
datastore_active: boolean;
|
|
35
|
-
last_modified: string | null;
|
|
36
|
-
created: string;
|
|
37
|
-
size: number | null;
|
|
38
|
-
position: number;
|
|
39
|
-
state: string;
|
|
40
|
-
mimetype: string | null;
|
|
41
|
-
}
|
|
42
|
-
interface CkanPackage {
|
|
43
|
-
id: string;
|
|
44
|
-
name: string;
|
|
45
|
-
title: string;
|
|
46
|
-
notes: string;
|
|
47
|
-
license_id: string;
|
|
48
|
-
license_title: string;
|
|
49
|
-
license_url?: string;
|
|
50
|
-
metadata_created: string;
|
|
51
|
-
metadata_modified: string;
|
|
52
|
-
num_resources: number;
|
|
53
|
-
organization: {
|
|
54
|
-
name: string;
|
|
55
|
-
title: string;
|
|
56
|
-
} | null;
|
|
57
|
-
resources: CkanResource[];
|
|
58
|
-
tags: {
|
|
59
|
-
name: string;
|
|
60
|
-
display_name: string;
|
|
61
|
-
}[];
|
|
62
|
-
extras: {
|
|
63
|
-
key: string;
|
|
64
|
-
value: string;
|
|
65
|
-
}[];
|
|
66
|
-
}
|
|
67
|
-
interface CkanPackageSearch {
|
|
68
|
-
count: number;
|
|
69
|
-
results: CkanPackage[];
|
|
70
|
-
}
|
|
71
|
-
/** Datastore column: `int4`, `numeric`, `text`, `date`, `timestamp`, `tsvector`… */
|
|
72
|
-
interface DatastoreField {
|
|
73
|
-
id: string;
|
|
74
|
-
type: string;
|
|
75
|
-
}
|
|
76
|
-
interface DatastoreSearchResult<Row = DatastoreRow> {
|
|
77
|
-
resource_id: string;
|
|
78
|
-
fields: DatastoreField[];
|
|
79
|
-
records: Row[];
|
|
80
|
-
total: number;
|
|
81
|
-
limit?: number;
|
|
82
|
-
offset?: number;
|
|
83
|
-
_links?: {
|
|
84
|
-
start?: string;
|
|
85
|
-
next?: string;
|
|
86
|
-
prev?: string;
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
interface DatastoreSqlResult<Row = DatastoreRow> {
|
|
90
|
-
sql: string;
|
|
91
|
-
fields: DatastoreField[];
|
|
92
|
-
records: Row[];
|
|
93
|
-
}
|
|
94
|
-
type DatastoreRow = Record<string, string | number | boolean | null>;
|
|
95
|
-
interface DatastoreSearchOptions {
|
|
96
|
-
/** Rows per request; the portal returns everything up to 32,000 in one page. */
|
|
97
|
-
limit?: number;
|
|
98
|
-
offset?: number;
|
|
99
|
-
/** e.g. `"_id"` or `"Settlement Date desc"`. */
|
|
100
|
-
sort?: string;
|
|
101
|
-
/** Equality filters, `{ "Constraint Group": "ESTEX" }`. */
|
|
102
|
-
filters?: Record<string, string | number | boolean | (string | number)[]>;
|
|
103
|
-
/** Columns to return. */
|
|
104
|
-
fields?: string[];
|
|
105
|
-
/** Full-text query. */
|
|
106
|
-
q?: string;
|
|
107
|
-
}
|
|
12
|
+
|
|
108
13
|
/** `thermal-constraint-costs`, one row per boundary per day. */
|
|
109
14
|
interface ThermalConstraintCostRow {
|
|
110
15
|
_id: number;
|
|
@@ -256,48 +161,14 @@ declare const PACKAGES: {
|
|
|
256
161
|
interface NesoClientOptions extends Omit<HttpClientOptions, "baseUrl"> {
|
|
257
162
|
baseUrl?: string;
|
|
258
163
|
}
|
|
259
|
-
|
|
260
|
-
* The SQL endpoint returns `numeric` columns as strings ("252"); `datastore_search` returns
|
|
261
|
-
* numbers. Make rows consistent: numeric-typed columns become numbers, `_full_text` goes.
|
|
262
|
-
*/
|
|
263
|
-
declare function normaliseRows<Row extends object>(records: readonly DatastoreRow[], fields: readonly DatastoreField[], extraNumeric?: readonly string[]): Row[];
|
|
264
|
-
/** Double-quote an identifier for the SQL endpoint. */
|
|
265
|
-
declare function ident(name: string): string;
|
|
266
|
-
/** Single-quote a literal for the SQL endpoint. */
|
|
267
|
-
declare function literal(value: string | number): string;
|
|
268
|
-
/**
|
|
269
|
-
* `SELECT * FROM "resource" WHERE "col" >= 'from' AND "col" <= 'to' ORDER BY …` — the shape
|
|
270
|
-
* every date-range helper uses. `toExclusive` switches the upper bound to `<` for timestamps.
|
|
271
|
-
*/
|
|
272
|
-
declare function rangeSql(resourceId: string, column: string, from: string, to: string, orderBy: readonly string[], opts?: {
|
|
273
|
-
toExclusive?: boolean;
|
|
274
|
-
where?: string | undefined;
|
|
275
|
-
}): string;
|
|
164
|
+
|
|
276
165
|
declare class NesoClient {
|
|
277
166
|
readonly http: HttpClient;
|
|
278
|
-
|
|
279
|
-
|
|
167
|
+
/** Raw CKAN API (`@uk-energy/core` `CkanClient`): `packageShow` (memoised), `datastoreSearch`, `datastoreAll`, `sql`… */
|
|
168
|
+
readonly ckan: CkanClient;
|
|
280
169
|
constructor(opts?: NesoClientOptions);
|
|
281
170
|
/** Any CKAN action; unwraps `{ success, result }` and throws `UkEnergyError` on `success: false`. */
|
|
282
171
|
action<T>(name: string, query?: Query): Promise<T>;
|
|
283
|
-
/** Raw CKAN API. */
|
|
284
|
-
readonly ckan: {
|
|
285
|
-
/** Dataset metadata and its resources (memoised for `cacheTtlMs`, default 10 minutes). */
|
|
286
|
-
packageShow: (id: string) => Promise<CkanPackage>;
|
|
287
|
-
/** Search datasets by free text. */
|
|
288
|
-
packageSearch: (q: string, rows?: number) => Promise<CkanPackageSearch>;
|
|
289
|
-
resourceShow: (id: string) => Promise<CkanResource>;
|
|
290
|
-
/** One page of a datastore resource (up to 32,000 rows). Rows are normalised (see `normaliseRows`). */
|
|
291
|
-
datastoreSearch: <Row extends object = DatastoreRow>(resourceId: string, opts?: DatastoreSearchOptions) => Promise<DatastoreSearchResult<Row>>;
|
|
292
|
-
/** Every row of a datastore resource, paging by `limit` (default 32,000). */
|
|
293
|
-
datastoreAll: <Row extends object = DatastoreRow>(resourceId: string, opts?: Omit<DatastoreSearchOptions, "offset">) => Promise<Row[]>;
|
|
294
|
-
/**
|
|
295
|
-
* `datastore_search_sql`: read-only PostgreSQL over the datastore. Resource ids are the
|
|
296
|
-
* table names (quote them). Numeric columns come back as strings from this endpoint; they
|
|
297
|
-
* are converted using the `fields` types, and `_full_text` is dropped.
|
|
298
|
-
*/
|
|
299
|
-
sql: <Row extends object = DatastoreRow>(sql: string, extraNumeric?: readonly string[]) => Promise<Row[]>;
|
|
300
|
-
};
|
|
301
172
|
/** Resolving resources inside datasets. */
|
|
302
173
|
readonly resources: {
|
|
303
174
|
/** All datastore-backed resources of a dataset. */
|
|
@@ -361,5 +232,5 @@ declare function financialYearInName(name: string): number | undefined;
|
|
|
361
232
|
/** The datastore resource of a dataset for one financial year, if any. */
|
|
362
233
|
declare function resourceForFinancialYear(resources: readonly CkanResource[], startYear: number): CkanResource | undefined;
|
|
363
234
|
|
|
364
|
-
export { BASE_URL, DEFAULT_CACHE_TTL_MS, DEFAULT_RATE_LIMIT, MAX_PAGE_SIZE, NesoClient, PACKAGES, financialYearInName, financialYearOf, financialYearsBetween,
|
|
365
|
-
export type {
|
|
235
|
+
export { BASE_URL, DEFAULT_CACHE_TTL_MS, DEFAULT_RATE_LIMIT, MAX_PAGE_SIZE, NesoClient, PACKAGES, financialYearInName, financialYearOf, financialYearsBetween, resourceForFinancialYear };
|
|
236
|
+
export type { ConstraintBreakdownRow, ConstraintForecastRow, ConstraintLimitRow, DailyBalancingCostRow, DayAheadFlowRow, DfsRequirementRow, DfsSummaryRow, DfsUtilisationRow, NesoClientOptions, ThermalConstraintCostRow };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { RateLimit, HttpClient, HttpClientOptions, Query, DateInput } from '@uk-energy/core';
|
|
1
|
+
import { RateLimit, HttpClient, CkanClient, HttpClientOptions, Query, CkanResource, DateInput } from '@uk-energy/core';
|
|
2
|
+
export { CkanFailure, CkanPackage, CkanPackageSearch, CkanResource, CkanResponse, CkanSuccess, DatastoreField, DatastoreRow, DatastoreSearchOptions, DatastoreSearchResult, DatastoreSqlResult, ident, literal, normaliseRows, rangeSql } from '@uk-energy/core';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Shapes of the NESO Data Portal (CKAN 2.8) API and of the datasets this package wraps.
|
|
@@ -8,103 +9,7 @@ import { RateLimit, HttpClient, HttpClientOptions, Query, DateInput } from '@uk-
|
|
|
8
9
|
*
|
|
9
10
|
* Supported by National Energy SO Open Data.
|
|
10
11
|
*/
|
|
11
|
-
|
|
12
|
-
help: string;
|
|
13
|
-
success: true;
|
|
14
|
-
result: T;
|
|
15
|
-
}
|
|
16
|
-
interface CkanFailure {
|
|
17
|
-
help: string;
|
|
18
|
-
success: false;
|
|
19
|
-
error: {
|
|
20
|
-
message?: string;
|
|
21
|
-
__type?: string;
|
|
22
|
-
[field: string]: unknown;
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
type CkanResponse<T> = CkanSuccess<T> | CkanFailure;
|
|
26
|
-
interface CkanResource {
|
|
27
|
-
id: string;
|
|
28
|
-
package_id: string;
|
|
29
|
-
name: string;
|
|
30
|
-
description: string;
|
|
31
|
-
format: string;
|
|
32
|
-
url: string;
|
|
33
|
-
/** True when the resource is loaded into the datastore (CSV/XLSX pushed to Postgres). */
|
|
34
|
-
datastore_active: boolean;
|
|
35
|
-
last_modified: string | null;
|
|
36
|
-
created: string;
|
|
37
|
-
size: number | null;
|
|
38
|
-
position: number;
|
|
39
|
-
state: string;
|
|
40
|
-
mimetype: string | null;
|
|
41
|
-
}
|
|
42
|
-
interface CkanPackage {
|
|
43
|
-
id: string;
|
|
44
|
-
name: string;
|
|
45
|
-
title: string;
|
|
46
|
-
notes: string;
|
|
47
|
-
license_id: string;
|
|
48
|
-
license_title: string;
|
|
49
|
-
license_url?: string;
|
|
50
|
-
metadata_created: string;
|
|
51
|
-
metadata_modified: string;
|
|
52
|
-
num_resources: number;
|
|
53
|
-
organization: {
|
|
54
|
-
name: string;
|
|
55
|
-
title: string;
|
|
56
|
-
} | null;
|
|
57
|
-
resources: CkanResource[];
|
|
58
|
-
tags: {
|
|
59
|
-
name: string;
|
|
60
|
-
display_name: string;
|
|
61
|
-
}[];
|
|
62
|
-
extras: {
|
|
63
|
-
key: string;
|
|
64
|
-
value: string;
|
|
65
|
-
}[];
|
|
66
|
-
}
|
|
67
|
-
interface CkanPackageSearch {
|
|
68
|
-
count: number;
|
|
69
|
-
results: CkanPackage[];
|
|
70
|
-
}
|
|
71
|
-
/** Datastore column: `int4`, `numeric`, `text`, `date`, `timestamp`, `tsvector`… */
|
|
72
|
-
interface DatastoreField {
|
|
73
|
-
id: string;
|
|
74
|
-
type: string;
|
|
75
|
-
}
|
|
76
|
-
interface DatastoreSearchResult<Row = DatastoreRow> {
|
|
77
|
-
resource_id: string;
|
|
78
|
-
fields: DatastoreField[];
|
|
79
|
-
records: Row[];
|
|
80
|
-
total: number;
|
|
81
|
-
limit?: number;
|
|
82
|
-
offset?: number;
|
|
83
|
-
_links?: {
|
|
84
|
-
start?: string;
|
|
85
|
-
next?: string;
|
|
86
|
-
prev?: string;
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
interface DatastoreSqlResult<Row = DatastoreRow> {
|
|
90
|
-
sql: string;
|
|
91
|
-
fields: DatastoreField[];
|
|
92
|
-
records: Row[];
|
|
93
|
-
}
|
|
94
|
-
type DatastoreRow = Record<string, string | number | boolean | null>;
|
|
95
|
-
interface DatastoreSearchOptions {
|
|
96
|
-
/** Rows per request; the portal returns everything up to 32,000 in one page. */
|
|
97
|
-
limit?: number;
|
|
98
|
-
offset?: number;
|
|
99
|
-
/** e.g. `"_id"` or `"Settlement Date desc"`. */
|
|
100
|
-
sort?: string;
|
|
101
|
-
/** Equality filters, `{ "Constraint Group": "ESTEX" }`. */
|
|
102
|
-
filters?: Record<string, string | number | boolean | (string | number)[]>;
|
|
103
|
-
/** Columns to return. */
|
|
104
|
-
fields?: string[];
|
|
105
|
-
/** Full-text query. */
|
|
106
|
-
q?: string;
|
|
107
|
-
}
|
|
12
|
+
|
|
108
13
|
/** `thermal-constraint-costs`, one row per boundary per day. */
|
|
109
14
|
interface ThermalConstraintCostRow {
|
|
110
15
|
_id: number;
|
|
@@ -256,48 +161,14 @@ declare const PACKAGES: {
|
|
|
256
161
|
interface NesoClientOptions extends Omit<HttpClientOptions, "baseUrl"> {
|
|
257
162
|
baseUrl?: string;
|
|
258
163
|
}
|
|
259
|
-
|
|
260
|
-
* The SQL endpoint returns `numeric` columns as strings ("252"); `datastore_search` returns
|
|
261
|
-
* numbers. Make rows consistent: numeric-typed columns become numbers, `_full_text` goes.
|
|
262
|
-
*/
|
|
263
|
-
declare function normaliseRows<Row extends object>(records: readonly DatastoreRow[], fields: readonly DatastoreField[], extraNumeric?: readonly string[]): Row[];
|
|
264
|
-
/** Double-quote an identifier for the SQL endpoint. */
|
|
265
|
-
declare function ident(name: string): string;
|
|
266
|
-
/** Single-quote a literal for the SQL endpoint. */
|
|
267
|
-
declare function literal(value: string | number): string;
|
|
268
|
-
/**
|
|
269
|
-
* `SELECT * FROM "resource" WHERE "col" >= 'from' AND "col" <= 'to' ORDER BY …` — the shape
|
|
270
|
-
* every date-range helper uses. `toExclusive` switches the upper bound to `<` for timestamps.
|
|
271
|
-
*/
|
|
272
|
-
declare function rangeSql(resourceId: string, column: string, from: string, to: string, orderBy: readonly string[], opts?: {
|
|
273
|
-
toExclusive?: boolean;
|
|
274
|
-
where?: string | undefined;
|
|
275
|
-
}): string;
|
|
164
|
+
|
|
276
165
|
declare class NesoClient {
|
|
277
166
|
readonly http: HttpClient;
|
|
278
|
-
|
|
279
|
-
|
|
167
|
+
/** Raw CKAN API (`@uk-energy/core` `CkanClient`): `packageShow` (memoised), `datastoreSearch`, `datastoreAll`, `sql`… */
|
|
168
|
+
readonly ckan: CkanClient;
|
|
280
169
|
constructor(opts?: NesoClientOptions);
|
|
281
170
|
/** Any CKAN action; unwraps `{ success, result }` and throws `UkEnergyError` on `success: false`. */
|
|
282
171
|
action<T>(name: string, query?: Query): Promise<T>;
|
|
283
|
-
/** Raw CKAN API. */
|
|
284
|
-
readonly ckan: {
|
|
285
|
-
/** Dataset metadata and its resources (memoised for `cacheTtlMs`, default 10 minutes). */
|
|
286
|
-
packageShow: (id: string) => Promise<CkanPackage>;
|
|
287
|
-
/** Search datasets by free text. */
|
|
288
|
-
packageSearch: (q: string, rows?: number) => Promise<CkanPackageSearch>;
|
|
289
|
-
resourceShow: (id: string) => Promise<CkanResource>;
|
|
290
|
-
/** One page of a datastore resource (up to 32,000 rows). Rows are normalised (see `normaliseRows`). */
|
|
291
|
-
datastoreSearch: <Row extends object = DatastoreRow>(resourceId: string, opts?: DatastoreSearchOptions) => Promise<DatastoreSearchResult<Row>>;
|
|
292
|
-
/** Every row of a datastore resource, paging by `limit` (default 32,000). */
|
|
293
|
-
datastoreAll: <Row extends object = DatastoreRow>(resourceId: string, opts?: Omit<DatastoreSearchOptions, "offset">) => Promise<Row[]>;
|
|
294
|
-
/**
|
|
295
|
-
* `datastore_search_sql`: read-only PostgreSQL over the datastore. Resource ids are the
|
|
296
|
-
* table names (quote them). Numeric columns come back as strings from this endpoint; they
|
|
297
|
-
* are converted using the `fields` types, and `_full_text` is dropped.
|
|
298
|
-
*/
|
|
299
|
-
sql: <Row extends object = DatastoreRow>(sql: string, extraNumeric?: readonly string[]) => Promise<Row[]>;
|
|
300
|
-
};
|
|
301
172
|
/** Resolving resources inside datasets. */
|
|
302
173
|
readonly resources: {
|
|
303
174
|
/** All datastore-backed resources of a dataset. */
|
|
@@ -361,5 +232,5 @@ declare function financialYearInName(name: string): number | undefined;
|
|
|
361
232
|
/** The datastore resource of a dataset for one financial year, if any. */
|
|
362
233
|
declare function resourceForFinancialYear(resources: readonly CkanResource[], startYear: number): CkanResource | undefined;
|
|
363
234
|
|
|
364
|
-
export { BASE_URL, DEFAULT_CACHE_TTL_MS, DEFAULT_RATE_LIMIT, MAX_PAGE_SIZE, NesoClient, PACKAGES, financialYearInName, financialYearOf, financialYearsBetween,
|
|
365
|
-
export type {
|
|
235
|
+
export { BASE_URL, DEFAULT_CACHE_TTL_MS, DEFAULT_RATE_LIMIT, MAX_PAGE_SIZE, NesoClient, PACKAGES, financialYearInName, financialYearOf, financialYearsBetween, resourceForFinancialYear };
|
|
236
|
+
export type { ConstraintBreakdownRow, ConstraintForecastRow, ConstraintLimitRow, DailyBalancingCostRow, DayAheadFlowRow, DfsRequirementRow, DfsSummaryRow, DfsUtilisationRow, NesoClientOptions, ThermalConstraintCostRow };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { HttpClient, UkEnergyError,
|
|
1
|
+
import { CKAN_MAX_PAGE_SIZE, HttpClient, CkanClient, UkEnergyError, rangeSql, ident, literal, londonDate } from '@uk-energy/core';
|
|
2
|
+
export { ident, literal, normaliseRows, rangeSql } from '@uk-energy/core';
|
|
2
3
|
|
|
3
4
|
// src/client.ts
|
|
4
5
|
|
|
@@ -28,12 +29,10 @@ function financialYearInName(name) {
|
|
|
28
29
|
function resourceForFinancialYear(resources, startYear) {
|
|
29
30
|
return resources.find((r) => r.datastore_active && financialYearInName(r.name) === startYear);
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
-
// src/client.ts
|
|
33
32
|
var BASE_URL = "https://api.neso.energy/api/3/action";
|
|
34
33
|
var DEFAULT_RATE_LIMIT = { requestsPerSecond: 5, burst: 10 };
|
|
35
34
|
var DEFAULT_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
36
|
-
var MAX_PAGE_SIZE =
|
|
35
|
+
var MAX_PAGE_SIZE = CKAN_MAX_PAGE_SIZE;
|
|
37
36
|
var PACKAGES = {
|
|
38
37
|
thermalConstraintCosts: "thermal-constraint-costs",
|
|
39
38
|
constraintBreakdown: "constraint-breakdown",
|
|
@@ -43,45 +42,6 @@ var PACKAGES = {
|
|
|
43
42
|
dailyBalancingCosts: "daily-balancing-costs-balancing-services-use-of-system",
|
|
44
43
|
demandFlexibility: "demand-flexibility"
|
|
45
44
|
};
|
|
46
|
-
var DECIMAL = /^-?\d+(\.\d+)?([eE][-+]?\d+)?$/;
|
|
47
|
-
var NUMERIC_TYPES = /* @__PURE__ */ new Set([
|
|
48
|
-
"numeric",
|
|
49
|
-
"int4",
|
|
50
|
-
"int8",
|
|
51
|
-
"float8",
|
|
52
|
-
"float4",
|
|
53
|
-
"int",
|
|
54
|
-
"double precision"
|
|
55
|
-
]);
|
|
56
|
-
function normaliseRows(records, fields, extraNumeric = []) {
|
|
57
|
-
const numeric = new Set(extraNumeric);
|
|
58
|
-
for (const f of fields) if (NUMERIC_TYPES.has(f.type)) numeric.add(f.id);
|
|
59
|
-
return records.map((r) => {
|
|
60
|
-
const out = {};
|
|
61
|
-
for (const [k, v] of Object.entries(r)) {
|
|
62
|
-
if (k === "_full_text") continue;
|
|
63
|
-
if (numeric.has(k) && typeof v === "string" && DECIMAL.test(v.trim())) {
|
|
64
|
-
const n = Number(v);
|
|
65
|
-
out[k] = Number.isFinite(n) ? n : v;
|
|
66
|
-
} else {
|
|
67
|
-
out[k] = v;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return out;
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
function ident(name) {
|
|
74
|
-
return `"${name.replace(/"/g, '""')}"`;
|
|
75
|
-
}
|
|
76
|
-
function literal(value) {
|
|
77
|
-
return typeof value === "number" ? String(value) : `'${value.replace(/'/g, "''")}'`;
|
|
78
|
-
}
|
|
79
|
-
function rangeSql(resourceId, column, from, to, orderBy, opts = {}) {
|
|
80
|
-
const upper = opts.toExclusive ? "<" : "<=";
|
|
81
|
-
const extra = opts.where ? ` AND ${opts.where}` : "";
|
|
82
|
-
const order = orderBy.length ? ` ORDER BY ${orderBy.map(ident).join(", ")}` : "";
|
|
83
|
-
return `SELECT * FROM ${ident(resourceId)} WHERE ${ident(column)} >= ${literal(from)} AND ${ident(column)} ${upper} ${literal(to)}${extra}${order}`;
|
|
84
|
-
}
|
|
85
45
|
function localRange(from, to) {
|
|
86
46
|
const f = londonDate(from);
|
|
87
47
|
const t = londonDate(to);
|
|
@@ -95,8 +55,8 @@ function nextDay(date) {
|
|
|
95
55
|
}
|
|
96
56
|
var NesoClient = class {
|
|
97
57
|
http;
|
|
98
|
-
|
|
99
|
-
|
|
58
|
+
/** Raw CKAN API (`@uk-energy/core` `CkanClient`): `packageShow` (memoised), `datastoreSearch`, `datastoreAll`, `sql`… */
|
|
59
|
+
ckan;
|
|
100
60
|
constructor(opts = {}) {
|
|
101
61
|
this.http = new HttpClient({
|
|
102
62
|
baseUrl: BASE_URL,
|
|
@@ -104,91 +64,19 @@ var NesoClient = class {
|
|
|
104
64
|
cacheTtlMs: opts.cache ? DEFAULT_CACHE_TTL_MS : 0,
|
|
105
65
|
...opts
|
|
106
66
|
});
|
|
107
|
-
this.
|
|
67
|
+
this.ckan = new CkanClient(this.http, {
|
|
68
|
+
packageTtlMs: opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,
|
|
69
|
+
maxPageSize: MAX_PAGE_SIZE
|
|
70
|
+
});
|
|
108
71
|
}
|
|
109
72
|
/** Any CKAN action; unwraps `{ success, result }` and throws `UkEnergyError` on `success: false`. */
|
|
110
73
|
async action(name, query) {
|
|
111
|
-
|
|
112
|
-
if (!res || typeof res !== "object" || !("success" in res)) {
|
|
113
|
-
throw new UkEnergyError(`Unexpected response shape for ${name}`);
|
|
114
|
-
}
|
|
115
|
-
if (!res.success) {
|
|
116
|
-
throw new UkEnergyError(`${name}: ${res.error?.message ?? JSON.stringify(res.error)}`);
|
|
117
|
-
}
|
|
118
|
-
return res.result;
|
|
74
|
+
return this.ckan.action(name, query);
|
|
119
75
|
}
|
|
120
|
-
/** Raw CKAN API. */
|
|
121
|
-
ckan = {
|
|
122
|
-
/** Dataset metadata and its resources (memoised for `cacheTtlMs`, default 10 minutes). */
|
|
123
|
-
packageShow: async (id) => {
|
|
124
|
-
const hit = this.packageCache.get(id);
|
|
125
|
-
if (hit && Date.now() - hit.at < this.packageTtlMs) return hit.value;
|
|
126
|
-
const value = this.action("package_show", { id }).then((pkg) => {
|
|
127
|
-
if (!pkg || !Array.isArray(pkg.resources)) {
|
|
128
|
-
throw new UkEnergyError(`package_show: unexpected shape for ${id}`);
|
|
129
|
-
}
|
|
130
|
-
return pkg;
|
|
131
|
-
});
|
|
132
|
-
this.packageCache.set(id, { at: Date.now(), value });
|
|
133
|
-
value.catch(() => this.packageCache.delete(id));
|
|
134
|
-
return value;
|
|
135
|
-
},
|
|
136
|
-
/** Search datasets by free text. */
|
|
137
|
-
packageSearch: async (q, rows = 20) => this.action("package_search", { q, rows }),
|
|
138
|
-
resourceShow: async (id) => this.action("resource_show", { id }),
|
|
139
|
-
/** One page of a datastore resource (up to 32,000 rows). Rows are normalised (see `normaliseRows`). */
|
|
140
|
-
datastoreSearch: async (resourceId, opts = {}) => {
|
|
141
|
-
const query = {
|
|
142
|
-
resource_id: resourceId,
|
|
143
|
-
limit: opts.limit ?? MAX_PAGE_SIZE,
|
|
144
|
-
sort: opts.sort ?? "_id"
|
|
145
|
-
};
|
|
146
|
-
if (opts.offset !== void 0) query.offset = opts.offset;
|
|
147
|
-
if (opts.filters) query.filters = JSON.stringify(opts.filters);
|
|
148
|
-
if (opts.fields) query.fields = opts.fields.join(",");
|
|
149
|
-
if (opts.q) query.q = opts.q;
|
|
150
|
-
const res = await this.action("datastore_search", query);
|
|
151
|
-
if (!Array.isArray(res?.records)) {
|
|
152
|
-
throw new UkEnergyError("Unexpected response shape for datastore_search");
|
|
153
|
-
}
|
|
154
|
-
return { ...res, records: normaliseRows(res.records, res.fields ?? []) };
|
|
155
|
-
},
|
|
156
|
-
/** Every row of a datastore resource, paging by `limit` (default 32,000). */
|
|
157
|
-
datastoreAll: async (resourceId, opts = {}) => {
|
|
158
|
-
const pageSize = Math.min(opts.limit ?? MAX_PAGE_SIZE, MAX_PAGE_SIZE);
|
|
159
|
-
const out = [];
|
|
160
|
-
for await (const page of pages({
|
|
161
|
-
fetchPage: (offset) => this.ckan.datastoreSearch(resourceId, {
|
|
162
|
-
...opts,
|
|
163
|
-
limit: pageSize,
|
|
164
|
-
offset: offset ?? 0
|
|
165
|
-
}),
|
|
166
|
-
items: (p) => p.records,
|
|
167
|
-
nextCursor: offsetCursor(pageSize, (p) => p.records.length)
|
|
168
|
-
})) {
|
|
169
|
-
out.push(...page.records);
|
|
170
|
-
}
|
|
171
|
-
return out;
|
|
172
|
-
},
|
|
173
|
-
/**
|
|
174
|
-
* `datastore_search_sql`: read-only PostgreSQL over the datastore. Resource ids are the
|
|
175
|
-
* table names (quote them). Numeric columns come back as strings from this endpoint; they
|
|
176
|
-
* are converted using the `fields` types, and `_full_text` is dropped.
|
|
177
|
-
*/
|
|
178
|
-
sql: async (sql, extraNumeric = []) => {
|
|
179
|
-
const res = await this.action("datastore_search_sql", {
|
|
180
|
-
sql
|
|
181
|
-
});
|
|
182
|
-
if (!Array.isArray(res?.records)) {
|
|
183
|
-
throw new UkEnergyError("Unexpected response shape for datastore_search_sql");
|
|
184
|
-
}
|
|
185
|
-
return normaliseRows(res.records, res.fields ?? [], extraNumeric);
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
76
|
/** Resolving resources inside datasets. */
|
|
189
77
|
resources = {
|
|
190
78
|
/** All datastore-backed resources of a dataset. */
|
|
191
|
-
datastore: async (packageId) =>
|
|
79
|
+
datastore: async (packageId) => this.ckan.datastoreResources(packageId),
|
|
192
80
|
/** The resource for a GB financial year (`2025` = April 2025 – March 2026), by name. */
|
|
193
81
|
forFinancialYear: async (packageId, startYear) => {
|
|
194
82
|
const pkg = await this.ckan.packageShow(packageId);
|
|
@@ -201,14 +89,7 @@ var NesoClient = class {
|
|
|
201
89
|
return r;
|
|
202
90
|
},
|
|
203
91
|
/** The datastore resource whose name matches exactly (or the regex), e.g. "DFS Utilisation Report". */
|
|
204
|
-
byName: async (packageId, name) =>
|
|
205
|
-
const list = await this.resources.datastore(packageId);
|
|
206
|
-
const r = list.find(
|
|
207
|
-
(x) => typeof name === "string" ? x.name.trim() === name.trim() : name.test(x.name)
|
|
208
|
-
);
|
|
209
|
-
if (!r) throw new UkEnergyError(`${packageId}: no datastore resource named ${String(name)}`);
|
|
210
|
-
return r;
|
|
211
|
-
}
|
|
92
|
+
byName: async (packageId, name) => this.ckan.resourceByName(packageId, name)
|
|
212
93
|
};
|
|
213
94
|
/** Rows of a per-financial-year dataset for an inclusive date range, across year boundaries. */
|
|
214
95
|
async byFinancialYear(packageId, column, range, orderBy, opts = {}) {
|
|
@@ -305,6 +186,6 @@ var NesoClient = class {
|
|
|
305
186
|
}
|
|
306
187
|
};
|
|
307
188
|
|
|
308
|
-
export { BASE_URL, DEFAULT_CACHE_TTL_MS, DEFAULT_RATE_LIMIT, MAX_PAGE_SIZE, NesoClient, PACKAGES, financialYearInName, financialYearOf, financialYearsBetween,
|
|
189
|
+
export { BASE_URL, DEFAULT_CACHE_TTL_MS, DEFAULT_RATE_LIMIT, MAX_PAGE_SIZE, NesoClient, PACKAGES, financialYearInName, financialYearOf, financialYearsBetween, resourceForFinancialYear };
|
|
309
190
|
//# sourceMappingURL=index.js.map
|
|
310
191
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resources.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Financial-year arithmetic and resource-name matching for datasets split one resource per\n * GB financial year (1 April – 31 March): \"Thermal Constraint Costs Data 25-26\",\n * \"Constraint Breakdown 2025-2026\", \"Daily Balancing Costs 2025-2026\".\n */\nimport type { CkanResource } from \"./types.js\";\n\n/** Financial year a date (\"YYYY-MM-DD\") falls in, as its starting calendar year. */\nexport function financialYearOf(date: string): number {\n const year = Number(date.slice(0, 4));\n const month = Number(date.slice(5, 7));\n return month >= 4 ? year : year - 1;\n}\n\n/** Starting years of every financial year touched by an inclusive date range. */\nexport function financialYearsBetween(from: string, to: string): number[] {\n const first = financialYearOf(from);\n const last = financialYearOf(to);\n const out: number[] = [];\n for (let y = first; y <= last; y++) out.push(y);\n return out;\n}\n\nconst YEAR_SPAN = /(?<!\\d)(\\d{4}|\\d{2})\\s*[-/–]\\s*(\\d{4}|\\d{2})(?!\\d)/g;\n\n/** The financial year a resource name refers to (\"25-26\" / \"2025-2026\" → 2025), or undefined. */\nexport function financialYearInName(name: string): number | undefined {\n for (const m of name.matchAll(YEAR_SPAN)) {\n if (!m[1] || !m[2]) continue;\n const start = m[1].length === 2 ? 2000 + Number(m[1]) : Number(m[1]);\n const end = m[2].length === 2 ? 2000 + Number(m[2]) : Number(m[2]);\n if (end === start + 1) return start;\n }\n return undefined;\n}\n\n/** The datastore resource of a dataset for one financial year, if any. */\nexport function resourceForFinancialYear(\n resources: readonly CkanResource[],\n startYear: number,\n): CkanResource | undefined {\n return resources.find((r) => r.datastore_active && financialYearInName(r.name) === startYear);\n}\n","/**\n * Client for the NESO Data Portal (https://www.neso.energy/data-portal), a CKAN 2.8 instance at\n * https://api.neso.energy/api/3/action/. No API key. Datasets (\"packages\") hold \"resources\";\n * tabular resources are loaded into a Postgres datastore that `datastore_search` and\n * `datastore_search_sql` query. Many datasets split one resource per GB financial year\n * (April–March), so the helpers resolve the right resource(s) for a date range.\n *\n * const neso = new NesoClient();\n * const costs = await neso.constraints.thermalCosts(\"2025-04-01\", \"2025-04-07\"); // £/day per boundary\n * const dfs = await neso.dfs.summary(\"2026-10-20\", \"2026-10-22\");\n * const rows = await neso.ckan.sql(`SELECT * FROM \"407f3e9d-…\" WHERE \"Settlement Date\" = '2025-04-01'`);\n *\n * Licence: National Energy SO Open Data Licence v1.0 (OGL v3 based): copy, adapt, use\n * commercially, with the attribution \"Supported by National Energy SO Open Data\".\n */\nimport {\n type DateInput,\n HttpClient,\n type HttpClientOptions,\n londonDate,\n offsetCursor,\n pages,\n type Query,\n type RateLimit,\n UkEnergyError,\n} from \"@uk-energy/core\";\nimport { financialYearsBetween, resourceForFinancialYear } from \"./resources.js\";\nimport type {\n CkanPackage,\n CkanPackageSearch,\n CkanResource,\n CkanResponse,\n ConstraintBreakdownRow,\n ConstraintForecastRow,\n ConstraintLimitRow,\n DailyBalancingCostRow,\n DatastoreField,\n DatastoreRow,\n DatastoreSearchOptions,\n DatastoreSearchResult,\n DatastoreSqlResult,\n DayAheadFlowRow,\n DfsRequirementRow,\n DfsSummaryRow,\n DfsUtilisationRow,\n ThermalConstraintCostRow,\n} from \"./types.js\";\n\nexport const BASE_URL = \"https://api.neso.energy/api/3/action\";\n\n/**\n * No limit is published; the licence only reserves the right to throttle \"excessive use\".\n * 5 rps / burst 10 is comfortably polite for a shared public CKAN.\n */\nexport const DEFAULT_RATE_LIMIT: RateLimit = { requestsPerSecond: 5, burst: 10 };\n\n/** Resources update daily or weekly; package metadata is memoised for this long. */\nexport const DEFAULT_CACHE_TTL_MS = 10 * 60 * 1000;\n\n/** The portal returns up to this many rows per `datastore_search` page. */\nexport const MAX_PAGE_SIZE = 32_000;\n\n/** Dataset (package) names on the portal that the helpers use. */\nexport const PACKAGES = {\n thermalConstraintCosts: \"thermal-constraint-costs\",\n constraintBreakdown: \"constraint-breakdown\",\n constraintCostForecast24m: \"24-months-ahead-constraint-cost-forecast\",\n constraintLimits24m: \"24-months-ahead-constraint-limits\",\n dayAheadConstraintFlows: \"day-ahead-constraint-flows-and-limits\",\n dailyBalancingCosts: \"daily-balancing-costs-balancing-services-use-of-system\",\n demandFlexibility: \"demand-flexibility\",\n} as const;\n\nexport interface NesoClientOptions extends Omit<HttpClientOptions, \"baseUrl\"> {\n baseUrl?: string;\n}\n\n/** Inclusive date range; strings are \"YYYY-MM-DD\", `Date`s are GB-local calendar days. */\ninterface DateRange {\n from: DateInput;\n to: DateInput;\n}\n\nconst DECIMAL = /^-?\\d+(\\.\\d+)?([eE][-+]?\\d+)?$/;\n\nconst NUMERIC_TYPES = new Set([\n \"numeric\",\n \"int4\",\n \"int8\",\n \"float8\",\n \"float4\",\n \"int\",\n \"double precision\",\n]);\n\n/**\n * The SQL endpoint returns `numeric` columns as strings (\"252\"); `datastore_search` returns\n * numbers. Make rows consistent: numeric-typed columns become numbers, `_full_text` goes.\n */\nexport function normaliseRows<Row extends object>(\n records: readonly DatastoreRow[],\n fields: readonly DatastoreField[],\n extraNumeric: readonly string[] = [],\n): Row[] {\n const numeric = new Set(extraNumeric);\n for (const f of fields) if (NUMERIC_TYPES.has(f.type)) numeric.add(f.id);\n return records.map((r) => {\n const out: DatastoreRow = {};\n for (const [k, v] of Object.entries(r)) {\n if (k === \"_full_text\") continue;\n if (numeric.has(k) && typeof v === \"string\" && DECIMAL.test(v.trim())) {\n const n = Number(v);\n out[k] = Number.isFinite(n) ? n : v;\n } else {\n out[k] = v;\n }\n }\n return out as Row;\n });\n}\n\n/** Double-quote an identifier for the SQL endpoint. */\nexport function ident(name: string): string {\n return `\"${name.replace(/\"/g, '\"\"')}\"`;\n}\n\n/** Single-quote a literal for the SQL endpoint. */\nexport function literal(value: string | number): string {\n return typeof value === \"number\" ? String(value) : `'${value.replace(/'/g, \"''\")}'`;\n}\n\n/**\n * `SELECT * FROM \"resource\" WHERE \"col\" >= 'from' AND \"col\" <= 'to' ORDER BY …` — the shape\n * every date-range helper uses. `toExclusive` switches the upper bound to `<` for timestamps.\n */\nexport function rangeSql(\n resourceId: string,\n column: string,\n from: string,\n to: string,\n orderBy: readonly string[],\n opts: { toExclusive?: boolean; where?: string | undefined } = {},\n): string {\n const upper = opts.toExclusive ? \"<\" : \"<=\";\n const extra = opts.where ? ` AND ${opts.where}` : \"\";\n const order = orderBy.length ? ` ORDER BY ${orderBy.map(ident).join(\", \")}` : \"\";\n return `SELECT * FROM ${ident(resourceId)} WHERE ${ident(column)} >= ${literal(from)} AND ${ident(column)} ${upper} ${literal(to)}${extra}${order}`;\n}\n\n/** Inclusive GB-local date range: `Date`s are read as Europe/London calendar days. */\nfunction localRange(from: DateInput, to: DateInput): [string, string] {\n const f = londonDate(from);\n const t = londonDate(to);\n if (f > t) throw new UkEnergyError(`from (${f}) is after to (${t})`);\n return [f, t];\n}\n\nfunction nextDay(date: string): string {\n const d = new Date(`${date}T00:00:00Z`);\n d.setUTCDate(d.getUTCDate() + 1);\n return d.toISOString().slice(0, 10);\n}\n\nexport class NesoClient {\n readonly http: HttpClient;\n private readonly packageCache = new Map<string, { at: number; value: Promise<CkanPackage> }>();\n private readonly packageTtlMs: number;\n\n constructor(opts: NesoClientOptions = {}) {\n this.http = new HttpClient({\n baseUrl: BASE_URL,\n rateLimit: DEFAULT_RATE_LIMIT,\n cacheTtlMs: opts.cache ? DEFAULT_CACHE_TTL_MS : 0,\n ...opts,\n });\n this.packageTtlMs = opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;\n }\n\n /** Any CKAN action; unwraps `{ success, result }` and throws `UkEnergyError` on `success: false`. */\n async action<T>(name: string, query?: Query): Promise<T> {\n const res = await this.http.get<CkanResponse<T>>(name, query);\n if (!res || typeof res !== \"object\" || !(\"success\" in res)) {\n throw new UkEnergyError(`Unexpected response shape for ${name}`);\n }\n if (!res.success) {\n throw new UkEnergyError(`${name}: ${res.error?.message ?? JSON.stringify(res.error)}`);\n }\n return res.result;\n }\n\n /** Raw CKAN API. */\n readonly ckan = {\n /** Dataset metadata and its resources (memoised for `cacheTtlMs`, default 10 minutes). */\n packageShow: async (id: string): Promise<CkanPackage> => {\n const hit = this.packageCache.get(id);\n if (hit && Date.now() - hit.at < this.packageTtlMs) return hit.value;\n const value = this.action<CkanPackage>(\"package_show\", { id }).then((pkg) => {\n if (!pkg || !Array.isArray(pkg.resources)) {\n throw new UkEnergyError(`package_show: unexpected shape for ${id}`);\n }\n return pkg;\n });\n this.packageCache.set(id, { at: Date.now(), value });\n value.catch(() => this.packageCache.delete(id));\n return value;\n },\n /** Search datasets by free text. */\n packageSearch: async (q: string, rows = 20): Promise<CkanPackageSearch> =>\n this.action(\"package_search\", { q, rows }),\n resourceShow: async (id: string): Promise<CkanResource> => this.action(\"resource_show\", { id }),\n /** One page of a datastore resource (up to 32,000 rows). Rows are normalised (see `normaliseRows`). */\n datastoreSearch: async <Row extends object = DatastoreRow>(\n resourceId: string,\n opts: DatastoreSearchOptions = {},\n ): Promise<DatastoreSearchResult<Row>> => {\n const query: Query = {\n resource_id: resourceId,\n limit: opts.limit ?? MAX_PAGE_SIZE,\n sort: opts.sort ?? \"_id\",\n };\n if (opts.offset !== undefined) query.offset = opts.offset;\n if (opts.filters) query.filters = JSON.stringify(opts.filters);\n if (opts.fields) query.fields = opts.fields.join(\",\");\n if (opts.q) query.q = opts.q;\n const res = await this.action<DatastoreSearchResult<DatastoreRow>>(\"datastore_search\", query);\n if (!Array.isArray(res?.records)) {\n throw new UkEnergyError(\"Unexpected response shape for datastore_search\");\n }\n return { ...res, records: normaliseRows<Row>(res.records, res.fields ?? []) };\n },\n /** Every row of a datastore resource, paging by `limit` (default 32,000). */\n datastoreAll: async <Row extends object = DatastoreRow>(\n resourceId: string,\n opts: Omit<DatastoreSearchOptions, \"offset\"> = {},\n ): Promise<Row[]> => {\n const pageSize = Math.min(opts.limit ?? MAX_PAGE_SIZE, MAX_PAGE_SIZE);\n const out: Row[] = [];\n for await (const page of pages<DatastoreSearchResult<Row>, Row, number>({\n fetchPage: (offset) =>\n this.ckan.datastoreSearch<Row>(resourceId, {\n ...opts,\n limit: pageSize,\n offset: offset ?? 0,\n }),\n items: (p) => p.records,\n nextCursor: offsetCursor(pageSize, (p) => p.records.length),\n })) {\n out.push(...page.records);\n }\n return out;\n },\n /**\n * `datastore_search_sql`: read-only PostgreSQL over the datastore. Resource ids are the\n * table names (quote them). Numeric columns come back as strings from this endpoint; they\n * are converted using the `fields` types, and `_full_text` is dropped.\n */\n sql: async <Row extends object = DatastoreRow>(\n sql: string,\n extraNumeric: readonly string[] = [],\n ): Promise<Row[]> => {\n const res = await this.action<DatastoreSqlResult<DatastoreRow>>(\"datastore_search_sql\", {\n sql,\n });\n if (!Array.isArray(res?.records)) {\n throw new UkEnergyError(\"Unexpected response shape for datastore_search_sql\");\n }\n return normaliseRows<Row>(res.records, res.fields ?? [], extraNumeric);\n },\n };\n\n /** Resolving resources inside datasets. */\n readonly resources = {\n /** All datastore-backed resources of a dataset. */\n datastore: async (packageId: string): Promise<CkanResource[]> =>\n (await this.ckan.packageShow(packageId)).resources.filter((r) => r.datastore_active),\n /** The resource for a GB financial year (`2025` = April 2025 – March 2026), by name. */\n forFinancialYear: async (packageId: string, startYear: number): Promise<CkanResource> => {\n const pkg = await this.ckan.packageShow(packageId);\n const r = resourceForFinancialYear(pkg.resources, startYear);\n if (!r) {\n throw new UkEnergyError(\n `${packageId}: no datastore resource for financial year ${startYear}/${(startYear + 1) % 100}`,\n );\n }\n return r;\n },\n /** The datastore resource whose name matches exactly (or the regex), e.g. \"DFS Utilisation Report\". */\n byName: async (packageId: string, name: string | RegExp): Promise<CkanResource> => {\n const list = await this.resources.datastore(packageId);\n const r = list.find((x) =>\n typeof name === \"string\" ? x.name.trim() === name.trim() : name.test(x.name),\n );\n if (!r) throw new UkEnergyError(`${packageId}: no datastore resource named ${String(name)}`);\n return r;\n },\n };\n\n /** Rows of a per-financial-year dataset for an inclusive date range, across year boundaries. */\n private async byFinancialYear<Row extends object>(\n packageId: string,\n column: string,\n range: DateRange,\n orderBy: readonly string[],\n opts: { where?: string | undefined; extraNumeric?: readonly string[] } = {},\n ): Promise<Row[]> {\n const [from, to] = localRange(range.from, range.to);\n const out: Row[] = [];\n for (const fy of financialYearsBetween(from, to)) {\n const resource = await this.resources.forFinancialYear(packageId, fy);\n const sql = rangeSql(resource.id, column, from, to, orderBy, { where: opts.where });\n out.push(...(await this.ckan.sql<Row>(sql, opts.extraNumeric)));\n }\n return out;\n }\n\n /** Transmission constraint costs and forecasts. */\n readonly constraints = {\n /**\n * Thermal constraint cost per boundary per day (£), `thermal-constraint-costs`. One row per\n * constraint group per settlement date, e.g. SCOTEX, SSHARN, SEIMP (Scottish export\n * boundaries), ESTEX, SWALEX.\n */\n thermalCosts: async (\n from: DateInput,\n to: DateInput,\n constraintGroup?: string,\n ): Promise<ThermalConstraintCostRow[]> =>\n this.byFinancialYear(\n PACKAGES.thermalConstraintCosts,\n \"Settlement Date\",\n { from, to },\n [\"Settlement Date\", \"Constraint Group\"],\n constraintGroup\n ? { where: `${ident(\"Constraint Group\")} = ${literal(constraintGroup)}` }\n : {},\n ),\n /**\n * Daily constraint cost (£) and volume (MWh) by type: thermal, voltage, inertia, largest\n * loss (`constraint-breakdown`). Thermal volume is negative (energy turned down).\n */\n breakdown: async (from: DateInput, to: DateInput): Promise<ConstraintBreakdownRow[]> =>\n this.byFinancialYear(PACKAGES.constraintBreakdown, \"Date\", { from, to }, [\"Date\"], {\n // \"Thermal constraints cost\" is typed text in some years\n extraNumeric: [\"Thermal constraints cost\"],\n }),\n /** NESO's 24-month-ahead constraint cost forecast, £m per month (24 rows). */\n forecast24m: async (): Promise<ConstraintForecastRow[]> => {\n const r = await this.resources.byName(PACKAGES.constraintCostForecast24m, /Forecast/);\n return this.ckan.datastoreAll<ConstraintForecastRow>(r.id);\n },\n /** 24-month-ahead boundary limits (MW) per week; boundary columns vary by publication. */\n limits24m: async (): Promise<ConstraintLimitRow[]> => {\n const r = await this.resources.byName(PACKAGES.constraintLimits24m, /limits/i);\n return this.ckan.datastoreAll<ConstraintLimitRow>(r.id);\n },\n /**\n * Day-ahead forecast flow vs limit per boundary, half-hourly, for a range of local dates\n * (`day-ahead-constraint-flows-and-limits`, ~750k rows, so always filtered by SQL).\n */\n dayAheadFlows: async (\n from: DateInput,\n to: DateInput,\n constraintGroup?: string,\n ): Promise<DayAheadFlowRow[]> => {\n const [f, t] = localRange(from, to);\n const r = await this.resources.byName(PACKAGES.dayAheadConstraintFlows, /Flows and Limits/);\n const sql = rangeSql(\n r.id,\n \"Date_ Time GMT_BST\",\n `${f}T00:00:00`,\n `${nextDay(t)}T00:00:00`,\n [\"Constraint Group\", \"Date_ Time GMT_BST\"],\n {\n toExclusive: true,\n where: constraintGroup\n ? `${ident(\"Constraint Group\")} = ${literal(constraintGroup)}`\n : undefined,\n },\n );\n return this.ckan.sql<DayAheadFlowRow>(sql);\n },\n /** Daily balancing (BSUoS) costs per settlement period by category, incl. `Constraints` (£). */\n dailyBalancingCosts: async (from: DateInput, to: DateInput): Promise<DailyBalancingCostRow[]> =>\n this.byFinancialYear(PACKAGES.dailyBalancingCosts, \"SETT_DATE\", { from, to }, [\n \"SETT_DATE\",\n \"SETT_PERIOD\",\n ]),\n };\n\n /** Demand Flexibility Service (current season; archives are separate resources). */\n readonly dfs = {\n /** Published requirements per event window, by delivery date. */\n requirements: async (from: DateInput, to: DateInput): Promise<DfsRequirementRow[]> =>\n this.dfsRange<DfsRequirementRow>(\"DFS Service Requirement\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n ]),\n /** Accepted/rejected bids per participant unit per event window. */\n utilisation: async (from: DateInput, to: DateInput): Promise<DfsUtilisationRow[]> =>\n this.dfsRange<DfsUtilisationRow>(\"DFS Utilisation Report\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n \"DFS Unit ID\",\n ]),\n /** Procured MW, accepted cost and settled volume/cost per event window. */\n summary: async (from: DateInput, to: DateInput): Promise<DfsSummaryRow[]> =>\n this.dfsRange<DfsSummaryRow>(\"DFS Utilisation Report Summary\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n ]),\n };\n\n private async dfsRange<Row extends object>(\n name: string,\n from: DateInput,\n to: DateInput,\n orderBy: readonly string[],\n ): Promise<Row[]> {\n const [f, t] = localRange(from, to);\n const r = await this.resources.byName(PACKAGES.demandFlexibility, name);\n return this.ckan.sql<Row>(rangeSql(r.id, \"Delivery Date\", f, t, orderBy));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/resources.ts","../src/client.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Financial-year arithmetic and resource-name matching for datasets split one resource per\n * GB financial year (1 April – 31 March): \"Thermal Constraint Costs Data 25-26\",\n * \"Constraint Breakdown 2025-2026\", \"Daily Balancing Costs 2025-2026\".\n */\nimport type { CkanResource } from \"./types.js\";\n\n/** Financial year a date (\"YYYY-MM-DD\") falls in, as its starting calendar year. */\nexport function financialYearOf(date: string): number {\n const year = Number(date.slice(0, 4));\n const month = Number(date.slice(5, 7));\n return month >= 4 ? year : year - 1;\n}\n\n/** Starting years of every financial year touched by an inclusive date range. */\nexport function financialYearsBetween(from: string, to: string): number[] {\n const first = financialYearOf(from);\n const last = financialYearOf(to);\n const out: number[] = [];\n for (let y = first; y <= last; y++) out.push(y);\n return out;\n}\n\nconst YEAR_SPAN = /(?<!\\d)(\\d{4}|\\d{2})\\s*[-/–]\\s*(\\d{4}|\\d{2})(?!\\d)/g;\n\n/** The financial year a resource name refers to (\"25-26\" / \"2025-2026\" → 2025), or undefined. */\nexport function financialYearInName(name: string): number | undefined {\n for (const m of name.matchAll(YEAR_SPAN)) {\n if (!m[1] || !m[2]) continue;\n const start = m[1].length === 2 ? 2000 + Number(m[1]) : Number(m[1]);\n const end = m[2].length === 2 ? 2000 + Number(m[2]) : Number(m[2]);\n if (end === start + 1) return start;\n }\n return undefined;\n}\n\n/** The datastore resource of a dataset for one financial year, if any. */\nexport function resourceForFinancialYear(\n resources: readonly CkanResource[],\n startYear: number,\n): CkanResource | undefined {\n return resources.find((r) => r.datastore_active && financialYearInName(r.name) === startYear);\n}\n","/**\n * Client for the NESO Data Portal (https://www.neso.energy/data-portal), a CKAN 2.8 instance at\n * https://api.neso.energy/api/3/action/. No API key. Datasets (\"packages\") hold \"resources\";\n * tabular resources are loaded into a Postgres datastore that `datastore_search` and\n * `datastore_search_sql` query. Many datasets split one resource per GB financial year\n * (April–March), so the helpers resolve the right resource(s) for a date range.\n *\n * const neso = new NesoClient();\n * const costs = await neso.constraints.thermalCosts(\"2025-04-01\", \"2025-04-07\"); // £/day per boundary\n * const dfs = await neso.dfs.summary(\"2026-10-20\", \"2026-10-22\");\n * const rows = await neso.ckan.sql(`SELECT * FROM \"407f3e9d-…\" WHERE \"Settlement Date\" = '2025-04-01'`);\n *\n * Licence: National Energy SO Open Data Licence v1.0 (OGL v3 based): copy, adapt, use\n * commercially, with the attribution \"Supported by National Energy SO Open Data\".\n */\nimport {\n CKAN_MAX_PAGE_SIZE,\n CkanClient,\n type CkanResource,\n type DateInput,\n HttpClient,\n type HttpClientOptions,\n ident,\n literal,\n londonDate,\n type Query,\n type RateLimit,\n rangeSql,\n UkEnergyError,\n} from \"@uk-energy/core\";\nimport { financialYearsBetween, resourceForFinancialYear } from \"./resources.js\";\nimport type {\n ConstraintBreakdownRow,\n ConstraintForecastRow,\n ConstraintLimitRow,\n DailyBalancingCostRow,\n DayAheadFlowRow,\n DfsRequirementRow,\n DfsSummaryRow,\n DfsUtilisationRow,\n ThermalConstraintCostRow,\n} from \"./types.js\";\n\nexport const BASE_URL = \"https://api.neso.energy/api/3/action\";\n\n/**\n * No limit is published; the licence only reserves the right to throttle \"excessive use\".\n * 5 rps / burst 10 is comfortably polite for a shared public CKAN.\n */\nexport const DEFAULT_RATE_LIMIT: RateLimit = { requestsPerSecond: 5, burst: 10 };\n\n/** Resources update daily or weekly; package metadata is memoised for this long. */\nexport const DEFAULT_CACHE_TTL_MS = 10 * 60 * 1000;\n\n/** The portal returns up to this many rows per `datastore_search` page. */\nexport const MAX_PAGE_SIZE = CKAN_MAX_PAGE_SIZE;\n\n/** Dataset (package) names on the portal that the helpers use. */\nexport const PACKAGES = {\n thermalConstraintCosts: \"thermal-constraint-costs\",\n constraintBreakdown: \"constraint-breakdown\",\n constraintCostForecast24m: \"24-months-ahead-constraint-cost-forecast\",\n constraintLimits24m: \"24-months-ahead-constraint-limits\",\n dayAheadConstraintFlows: \"day-ahead-constraint-flows-and-limits\",\n dailyBalancingCosts: \"daily-balancing-costs-balancing-services-use-of-system\",\n demandFlexibility: \"demand-flexibility\",\n} as const;\n\nexport interface NesoClientOptions extends Omit<HttpClientOptions, \"baseUrl\"> {\n baseUrl?: string;\n}\n\n/** Inclusive date range; strings are \"YYYY-MM-DD\", `Date`s are GB-local calendar days. */\ninterface DateRange {\n from: DateInput;\n to: DateInput;\n}\n\nexport { ident, literal, normaliseRows, rangeSql } from \"@uk-energy/core\";\n\n/** Inclusive GB-local date range: `Date`s are read as Europe/London calendar days. */\nfunction localRange(from: DateInput, to: DateInput): [string, string] {\n const f = londonDate(from);\n const t = londonDate(to);\n if (f > t) throw new UkEnergyError(`from (${f}) is after to (${t})`);\n return [f, t];\n}\n\nfunction nextDay(date: string): string {\n const d = new Date(`${date}T00:00:00Z`);\n d.setUTCDate(d.getUTCDate() + 1);\n return d.toISOString().slice(0, 10);\n}\n\nexport class NesoClient {\n readonly http: HttpClient;\n /** Raw CKAN API (`@uk-energy/core` `CkanClient`): `packageShow` (memoised), `datastoreSearch`, `datastoreAll`, `sql`… */\n readonly ckan: CkanClient;\n\n constructor(opts: NesoClientOptions = {}) {\n this.http = new HttpClient({\n baseUrl: BASE_URL,\n rateLimit: DEFAULT_RATE_LIMIT,\n cacheTtlMs: opts.cache ? DEFAULT_CACHE_TTL_MS : 0,\n ...opts,\n });\n this.ckan = new CkanClient(this.http, {\n packageTtlMs: opts.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS,\n maxPageSize: MAX_PAGE_SIZE,\n });\n }\n\n /** Any CKAN action; unwraps `{ success, result }` and throws `UkEnergyError` on `success: false`. */\n async action<T>(name: string, query?: Query): Promise<T> {\n return this.ckan.action<T>(name, query);\n }\n\n /** Resolving resources inside datasets. */\n readonly resources = {\n /** All datastore-backed resources of a dataset. */\n datastore: async (packageId: string): Promise<CkanResource[]> =>\n this.ckan.datastoreResources(packageId),\n /** The resource for a GB financial year (`2025` = April 2025 – March 2026), by name. */\n forFinancialYear: async (packageId: string, startYear: number): Promise<CkanResource> => {\n const pkg = await this.ckan.packageShow(packageId);\n const r = resourceForFinancialYear(pkg.resources, startYear);\n if (!r) {\n throw new UkEnergyError(\n `${packageId}: no datastore resource for financial year ${startYear}/${(startYear + 1) % 100}`,\n );\n }\n return r;\n },\n /** The datastore resource whose name matches exactly (or the regex), e.g. \"DFS Utilisation Report\". */\n byName: async (packageId: string, name: string | RegExp): Promise<CkanResource> =>\n this.ckan.resourceByName(packageId, name),\n };\n\n /** Rows of a per-financial-year dataset for an inclusive date range, across year boundaries. */\n private async byFinancialYear<Row extends object>(\n packageId: string,\n column: string,\n range: DateRange,\n orderBy: readonly string[],\n opts: { where?: string | undefined; extraNumeric?: readonly string[] } = {},\n ): Promise<Row[]> {\n const [from, to] = localRange(range.from, range.to);\n const out: Row[] = [];\n for (const fy of financialYearsBetween(from, to)) {\n const resource = await this.resources.forFinancialYear(packageId, fy);\n const sql = rangeSql(resource.id, column, from, to, orderBy, { where: opts.where });\n out.push(...(await this.ckan.sql<Row>(sql, opts.extraNumeric)));\n }\n return out;\n }\n\n /** Transmission constraint costs and forecasts. */\n readonly constraints = {\n /**\n * Thermal constraint cost per boundary per day (£), `thermal-constraint-costs`. One row per\n * constraint group per settlement date, e.g. SCOTEX, SSHARN, SEIMP (Scottish export\n * boundaries), ESTEX, SWALEX.\n */\n thermalCosts: async (\n from: DateInput,\n to: DateInput,\n constraintGroup?: string,\n ): Promise<ThermalConstraintCostRow[]> =>\n this.byFinancialYear(\n PACKAGES.thermalConstraintCosts,\n \"Settlement Date\",\n { from, to },\n [\"Settlement Date\", \"Constraint Group\"],\n constraintGroup\n ? { where: `${ident(\"Constraint Group\")} = ${literal(constraintGroup)}` }\n : {},\n ),\n /**\n * Daily constraint cost (£) and volume (MWh) by type: thermal, voltage, inertia, largest\n * loss (`constraint-breakdown`). Thermal volume is negative (energy turned down).\n */\n breakdown: async (from: DateInput, to: DateInput): Promise<ConstraintBreakdownRow[]> =>\n this.byFinancialYear(PACKAGES.constraintBreakdown, \"Date\", { from, to }, [\"Date\"], {\n // \"Thermal constraints cost\" is typed text in some years\n extraNumeric: [\"Thermal constraints cost\"],\n }),\n /** NESO's 24-month-ahead constraint cost forecast, £m per month (24 rows). */\n forecast24m: async (): Promise<ConstraintForecastRow[]> => {\n const r = await this.resources.byName(PACKAGES.constraintCostForecast24m, /Forecast/);\n return this.ckan.datastoreAll<ConstraintForecastRow>(r.id);\n },\n /** 24-month-ahead boundary limits (MW) per week; boundary columns vary by publication. */\n limits24m: async (): Promise<ConstraintLimitRow[]> => {\n const r = await this.resources.byName(PACKAGES.constraintLimits24m, /limits/i);\n return this.ckan.datastoreAll<ConstraintLimitRow>(r.id);\n },\n /**\n * Day-ahead forecast flow vs limit per boundary, half-hourly, for a range of local dates\n * (`day-ahead-constraint-flows-and-limits`, ~750k rows, so always filtered by SQL).\n */\n dayAheadFlows: async (\n from: DateInput,\n to: DateInput,\n constraintGroup?: string,\n ): Promise<DayAheadFlowRow[]> => {\n const [f, t] = localRange(from, to);\n const r = await this.resources.byName(PACKAGES.dayAheadConstraintFlows, /Flows and Limits/);\n const sql = rangeSql(\n r.id,\n \"Date_ Time GMT_BST\",\n `${f}T00:00:00`,\n `${nextDay(t)}T00:00:00`,\n [\"Constraint Group\", \"Date_ Time GMT_BST\"],\n {\n toExclusive: true,\n where: constraintGroup\n ? `${ident(\"Constraint Group\")} = ${literal(constraintGroup)}`\n : undefined,\n },\n );\n return this.ckan.sql<DayAheadFlowRow>(sql);\n },\n /** Daily balancing (BSUoS) costs per settlement period by category, incl. `Constraints` (£). */\n dailyBalancingCosts: async (from: DateInput, to: DateInput): Promise<DailyBalancingCostRow[]> =>\n this.byFinancialYear(PACKAGES.dailyBalancingCosts, \"SETT_DATE\", { from, to }, [\n \"SETT_DATE\",\n \"SETT_PERIOD\",\n ]),\n };\n\n /** Demand Flexibility Service (current season; archives are separate resources). */\n readonly dfs = {\n /** Published requirements per event window, by delivery date. */\n requirements: async (from: DateInput, to: DateInput): Promise<DfsRequirementRow[]> =>\n this.dfsRange<DfsRequirementRow>(\"DFS Service Requirement\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n ]),\n /** Accepted/rejected bids per participant unit per event window. */\n utilisation: async (from: DateInput, to: DateInput): Promise<DfsUtilisationRow[]> =>\n this.dfsRange<DfsUtilisationRow>(\"DFS Utilisation Report\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n \"DFS Unit ID\",\n ]),\n /** Procured MW, accepted cost and settled volume/cost per event window. */\n summary: async (from: DateInput, to: DateInput): Promise<DfsSummaryRow[]> =>\n this.dfsRange<DfsSummaryRow>(\"DFS Utilisation Report Summary\", from, to, [\n \"Event ID\",\n \"From_UTC\",\n ]),\n };\n\n private async dfsRange<Row extends object>(\n name: string,\n from: DateInput,\n to: DateInput,\n orderBy: readonly string[],\n ): Promise<Row[]> {\n const [f, t] = localRange(from, to);\n const r = await this.resources.byName(PACKAGES.demandFlexibility, name);\n return this.ckan.sql<Row>(rangeSql(r.id, \"Delivery Date\", f, t, orderBy));\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uk-energy/neso",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Typed client for the NESO Data Portal (CKAN): datastore search and SQL, thermal constraint costs, constraint breakdown and forecasts, day-ahead boundary flows, Demand Flexibility Service, daily balancing costs. Part of uk-energy-sdk.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"uk-energy"
|
|
45
45
|
],
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@uk-energy/core": "^0.
|
|
47
|
+
"@uk-energy/core": "^0.3.0"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"access": "public"
|