@substrio/sdk 0.0.0-stage → 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/LICENSE +177 -0
- package/README.md +39 -2
- package/package.json +52 -5
- package/substrio.d.ts +1087 -0
- package/substrio.mjs +3928 -0
- package/substrio_errors.mjs +345 -0
- package/substrio_kv_tcp.mjs +559 -0
- package/substrio_operation_client.d.mts +28 -0
- package/substrio_operation_client.mjs +196 -0
- package/substrio_operations.d.mts +11998 -0
- package/substrio_operations.mjs +1731 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Substrio SDK: every registry operation as a typed method, grouped by the
|
|
3
|
+
* words of its dotted name the way the CLI groups them: kv.get is
|
|
4
|
+
* client.ops.kv.get(input), account.key.create is
|
|
5
|
+
* client.ops.account.key.create(input).
|
|
6
|
+
*
|
|
7
|
+
* What an operation is (its routes, path and query parameters, whether it
|
|
8
|
+
* takes a JSON body) comes only from substrio_operations.mjs, which
|
|
9
|
+
* scripts/gen_operations.mjs generates from the server's catalog
|
|
10
|
+
* (GET /v1/_openapi, projected from src/substrio/core/substrio_operation_registry.h).
|
|
11
|
+
* Nothing here knows one operation from another. Types for every input,
|
|
12
|
+
* result and the error envelope are in substrio_operations.d.mts.
|
|
13
|
+
*
|
|
14
|
+
* An input is one object: each {placeholder} of the route takes its field,
|
|
15
|
+
* declared query parameters go in the query string, and the rest is the
|
|
16
|
+
* query string (GET, HEAD, DELETE) or the JSON body. A route whose body is
|
|
17
|
+
* one input property's value (bodyProperty, such as kv.set's entries) sends
|
|
18
|
+
* that value as the body. A non-object input is the body as it is.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { OPERATIONS } from './substrio_operations.mjs';
|
|
22
|
+
|
|
23
|
+
// Methods whose remaining inputs travel in the query string.
|
|
24
|
+
const QUERY_METHODS = new Set(['GET', 'HEAD', 'DELETE']);
|
|
25
|
+
|
|
26
|
+
const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
27
|
+
const text = (value) => (typeof value === 'string' ? value : JSON.stringify(value));
|
|
28
|
+
const absent = (value) => value === undefined || value === null;
|
|
29
|
+
|
|
30
|
+
/** The {placeholders} of a route path, in order (pure). */
|
|
31
|
+
export function routeParams(path) {
|
|
32
|
+
return [...path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One route of the server's catalog (GET /v1/_openapi) as this module calls
|
|
37
|
+
* it (pure): `op` is paths[path][method]. {method, path, params, query,
|
|
38
|
+
* parameters, body, bodyProperty, bodySchema}: `body` is true when the route
|
|
39
|
+
* takes a JSON body, and `bodyProperty` names the input property whose value
|
|
40
|
+
* is that whole body (x-substrio-body-property), else null.
|
|
41
|
+
*/
|
|
42
|
+
export function catalogRoute(path, method, op) {
|
|
43
|
+
const parameters = Array.isArray(op?.parameters) ? op.parameters : [];
|
|
44
|
+
const bodySchema = op?.requestBody?.content?.['application/json']?.schema ?? null;
|
|
45
|
+
return {
|
|
46
|
+
method: method.toUpperCase(),
|
|
47
|
+
path,
|
|
48
|
+
params: routeParams(path),
|
|
49
|
+
query: parameters.filter((p) => p.in === 'query').map((p) => p.name),
|
|
50
|
+
parameters,
|
|
51
|
+
body: bodySchema !== null,
|
|
52
|
+
bodyProperty: op?.['x-substrio-body-property'] ?? null,
|
|
53
|
+
bodySchema,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The route of an operation to call with `input` (pure): among its routes
|
|
59
|
+
* (those with `method`, when given) whose path parameters the input fills,
|
|
60
|
+
* the one that carries a JSON body (any field fits there), else the first
|
|
61
|
+
* that fits, else the first (its error names what is missing).
|
|
62
|
+
*/
|
|
63
|
+
export function pickRoute(operation, input, method = null) {
|
|
64
|
+
const candidates = method ? operation.routes.filter((r) => r.method === method) : operation.routes;
|
|
65
|
+
if (candidates.length === 0) {
|
|
66
|
+
throw new TypeError(`${operation.name} has no ${method} route (it has ${[...new Set(operation.routes.map((r) => r.method))].join(', ')})`);
|
|
67
|
+
}
|
|
68
|
+
const fitting = candidates.filter((r) => r.params.every((p) => isObject(input) && !absent(input[p])));
|
|
69
|
+
return fitting.find((r) => r.body) || fitting[0] || candidates[0];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The request for one route and an input (pure): {method, path, body}.
|
|
74
|
+
* Throws when a path parameter is missing. With `rawBody` the caller sends the
|
|
75
|
+
* body itself as raw bytes (data.import's NDJSON, file.upload): the route's
|
|
76
|
+
* JSON body field is neither required nor built.
|
|
77
|
+
*/
|
|
78
|
+
export function buildRequest(route, input, { rawBody = false } = {}) {
|
|
79
|
+
const rest = isObject(input) ? { ...input } : {};
|
|
80
|
+
const missing = route.params.filter((p) => absent(rest[p]));
|
|
81
|
+
if (missing.length) throw new TypeError(`missing ${missing.join(', ')} (the route is ${route.method} ${route.path})`);
|
|
82
|
+
const path = route.path.replace(/\{([^}]+)\}/g, (_, name) => {
|
|
83
|
+
const value = rest[name];
|
|
84
|
+
delete rest[name];
|
|
85
|
+
return encodeURIComponent(text(value));
|
|
86
|
+
});
|
|
87
|
+
const query = new URLSearchParams();
|
|
88
|
+
for (const name of route.query) {
|
|
89
|
+
if (!absent(rest[name])) query.append(name, text(rest[name]));
|
|
90
|
+
delete rest[name];
|
|
91
|
+
}
|
|
92
|
+
let body;
|
|
93
|
+
if (rawBody) {
|
|
94
|
+
body = undefined;
|
|
95
|
+
} else if (route.bodyProperty) {
|
|
96
|
+
if (absent(rest[route.bodyProperty])) {
|
|
97
|
+
throw new TypeError(`missing ${route.bodyProperty} (the body of ${route.method} ${route.path})`);
|
|
98
|
+
}
|
|
99
|
+
body = rest[route.bodyProperty];
|
|
100
|
+
} else if (QUERY_METHODS.has(route.method)) {
|
|
101
|
+
if (input !== undefined && !isObject(input)) {
|
|
102
|
+
throw new TypeError(`${route.method} ${route.path} takes named fields, not a bare JSON value`);
|
|
103
|
+
}
|
|
104
|
+
for (const [key, value] of Object.entries(rest)) if (!absent(value)) query.append(key, text(value));
|
|
105
|
+
} else {
|
|
106
|
+
body = isObject(input) ? rest : (input === undefined ? {} : input);
|
|
107
|
+
}
|
|
108
|
+
const qs = query.toString();
|
|
109
|
+
return { method: route.method, path: qs ? `${path}?${qs}` : path, body };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Calls `operation` over `http` (the SDK's Http: req(method, path, body)). */
|
|
113
|
+
export function callOperation(http, operation, input, options = {}) {
|
|
114
|
+
const request = buildRequest(pickRoute(operation, input, options.method ?? null), input);
|
|
115
|
+
return http.req(request.method, request.path, request.body);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The operation tree for `http`: one method per operation at the path of
|
|
120
|
+
* its name's words. A name that is also the prefix of others (function.logs,
|
|
121
|
+
* function.logs.clear) is a method that carries its children.
|
|
122
|
+
*/
|
|
123
|
+
export function bindOperations(http) {
|
|
124
|
+
const root = {};
|
|
125
|
+
for (const operation of OPERATIONS) {
|
|
126
|
+
const words = operation.words;
|
|
127
|
+
let node = root;
|
|
128
|
+
for (const word of words.slice(0, -1)) node = node[word] ??= {};
|
|
129
|
+
const last = words[words.length - 1];
|
|
130
|
+
const call = (input, options) => callOperation(http, operation, input, options);
|
|
131
|
+
Object.defineProperty(call, 'name', { value: operation.name });
|
|
132
|
+
Object.defineProperty(call, 'operation', { value: operation });
|
|
133
|
+
// Children bound before their parent move onto the parent's method.
|
|
134
|
+
node[last] = Object.assign(call, node[last]);
|
|
135
|
+
}
|
|
136
|
+
return deepFreeze(root);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function deepFreeze(node) {
|
|
140
|
+
for (const child of Object.values(node)) deepFreeze(child);
|
|
141
|
+
return Object.freeze(node);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Makes each operation group reachable on `client` by its name (client.kv.get)
|
|
146
|
+
* next to client.ops. A group whose name the client already uses keeps the
|
|
147
|
+
* client's member; that member takes the group's operations whose names it
|
|
148
|
+
* does not have (a method gets its own bound copy), non-enumerable, so
|
|
149
|
+
* nothing the client already offered changes.
|
|
150
|
+
*/
|
|
151
|
+
export function attachOperations(client, ops) {
|
|
152
|
+
for (const [group, node] of Object.entries(ops)) {
|
|
153
|
+
if (!(group in client)) {
|
|
154
|
+
Object.defineProperty(client, group, { value: node, enumerable: false });
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const descriptor = findDescriptor(client, group);
|
|
158
|
+
if (typeof descriptor?.value === 'function') {
|
|
159
|
+
const method = descriptor.value.bind(client);
|
|
160
|
+
addMissing(method, node);
|
|
161
|
+
Object.defineProperty(client, group, { value: method, enumerable: false, writable: true, configurable: true });
|
|
162
|
+
} else if (descriptor?.get) {
|
|
163
|
+
// A getter's object is made when first read: it takes the group then.
|
|
164
|
+
const read = descriptor.get;
|
|
165
|
+
const merged = new WeakSet();
|
|
166
|
+
Object.defineProperty(client, group, {
|
|
167
|
+
enumerable: false,
|
|
168
|
+
configurable: true,
|
|
169
|
+
get() {
|
|
170
|
+
const value = read.call(this);
|
|
171
|
+
if (value !== null && typeof value === 'object' && !merged.has(value)) {
|
|
172
|
+
addMissing(value, node);
|
|
173
|
+
merged.add(value);
|
|
174
|
+
}
|
|
175
|
+
return value;
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
} else if (descriptor && descriptor.value !== null && typeof descriptor.value === 'object') {
|
|
179
|
+
addMissing(descriptor.value, node);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function findDescriptor(object, name) {
|
|
185
|
+
for (let o = object; o; o = Object.getPrototypeOf(o)) {
|
|
186
|
+
const d = Object.getOwnPropertyDescriptor(o, name);
|
|
187
|
+
if (d) return d;
|
|
188
|
+
}
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function addMissing(target, node) {
|
|
193
|
+
for (const [word, child] of Object.entries(node)) {
|
|
194
|
+
if (!(word in target)) Object.defineProperty(target, word, { value: child, enumerable: false });
|
|
195
|
+
}
|
|
196
|
+
}
|