@graphty/graph-io 0.3.49 → 0.3.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{approximations-Bzn3L-je.js → approximations-BRXsll0T.js} +2 -2
- package/dist/chunks/{approximations-Bzn3L-je.js.map → approximations-BRXsll0T.js.map} +1 -1
- package/dist/chunks/{block-iptrlMBb.js → block-yl0f_qLO.js} +27 -2
- package/dist/chunks/block-yl0f_qLO.js.map +1 -0
- package/dist/chunks/{context-C3p0RQxR.js → context-DizmppHy.js} +2 -2
- package/dist/chunks/{context-C3p0RQxR.js.map → context-DizmppHy.js.map} +1 -1
- package/dist/chunks/{credentials-DXHx7nwR.js → credentials-CmvJXYeH.js} +58 -9
- package/dist/chunks/credentials-CmvJXYeH.js.map +1 -0
- package/dist/chunks/{descriptor-DLA5kINx.js → descriptor-Dhez_Cew.js} +5 -1
- package/dist/chunks/{descriptor-DLA5kINx.js.map → descriptor-Dhez_Cew.js.map} +1 -1
- package/dist/chunks/{emit-BwHQkZle.js → emit-7Jkpg0yd.js} +132 -4
- package/dist/chunks/emit-7Jkpg0yd.js.map +1 -0
- package/dist/chunks/{exporter-nDfxIK5D.js → exporter-1Ajud15J.js} +4 -4
- package/dist/chunks/{exporter-nDfxIK5D.js.map → exporter-1Ajud15J.js.map} +1 -1
- package/dist/chunks/{exporter-D9yMtocA.js → exporter-B6yvL3wy.js} +3 -3
- package/dist/chunks/{exporter-D9yMtocA.js.map → exporter-B6yvL3wy.js.map} +1 -1
- package/dist/chunks/{exporter-_jrBTnos.js → exporter-CA5q8ROP.js} +4 -4
- package/dist/chunks/{exporter-_jrBTnos.js.map → exporter-CA5q8ROP.js.map} +1 -1
- package/dist/chunks/{exporter-D2CETOMK.js → exporter-CQQZFJ2v.js} +72 -10
- package/dist/chunks/exporter-CQQZFJ2v.js.map +1 -0
- package/dist/chunks/{generic-ymWCETAJ.js → generic-Dc21MAdg.js} +3 -3
- package/dist/chunks/{generic-ymWCETAJ.js.map → generic-Dc21MAdg.js.map} +1 -1
- package/dist/chunks/{importer-CT9mJ1nn.js → importer-C6NVpCXp.js} +7 -6
- package/dist/chunks/importer-C6NVpCXp.js.map +1 -0
- package/dist/chunks/{importer-GREKMdIf.js → importer-CKFe4q54.js} +4 -4
- package/dist/chunks/{importer-GREKMdIf.js.map → importer-CKFe4q54.js.map} +1 -1
- package/dist/chunks/{importer-PqaaKKnY.js → importer-CKbUbsJA.js} +4 -4
- package/dist/chunks/{importer-PqaaKKnY.js.map → importer-CKbUbsJA.js.map} +1 -1
- package/dist/chunks/{importer-DaLTjbds.js → importer-ClCsdgTJ.js} +97 -7
- package/dist/chunks/importer-ClCsdgTJ.js.map +1 -0
- package/dist/chunks/{importer-B92x1xY5.js → importer-CyVZTrf0.js} +3 -3
- package/dist/chunks/{importer-B92x1xY5.js.map → importer-CyVZTrf0.js.map} +1 -1
- package/dist/chunks/{importer-vRq4h3j5.js → importer-DgG4v1Ms.js} +89 -20
- package/dist/chunks/importer-DgG4v1Ms.js.map +1 -0
- package/dist/chunks/{importer-Vn5QvmmJ.js → importer-asFUrIsj.js} +2 -2
- package/dist/chunks/{importer-Vn5QvmmJ.js.map → importer-asFUrIsj.js.map} +1 -1
- package/dist/chunks/{lexicon-Clf4QIyl.js → lexicon-IVRq2Uoo.js} +2 -2
- package/dist/chunks/{lexicon-Clf4QIyl.js.map → lexicon-IVRq2Uoo.js.map} +1 -1
- package/dist/chunks/{round-trip-Zm3_ivgq.js → round-trip-Cy3_SHRZ.js} +2 -2
- package/dist/chunks/{round-trip-Zm3_ivgq.js.map → round-trip-Cy3_SHRZ.js.map} +1 -1
- package/dist/chunks/{sigv4-EokFic1V.js → sigv4-BXwVbza3.js} +3 -3
- package/dist/chunks/{sigv4-EokFic1V.js.map → sigv4-BXwVbza3.js.map} +1 -1
- package/dist/chunks/{styles-CCs214_3.js → styles-CAVmBLgp.js} +4 -4
- package/dist/chunks/{styles-CCs214_3.js.map → styles-CAVmBLgp.js.map} +1 -1
- package/dist/chunks/{styles-DPQQIig4.js → styles-CPczw1Jz.js} +4 -4
- package/dist/chunks/{styles-DPQQIig4.js.map → styles-CPczw1Jz.js.map} +1 -1
- package/dist/chunks/{styles-Cm62yyQX.js → styles-boK6Ln54.js} +65 -30
- package/dist/chunks/styles-boK6Ln54.js.map +1 -0
- package/dist/chunks/{styles-yed-UMFdNqze.js → styles-yed-XeFBQ6VS.js} +8 -8
- package/dist/chunks/{styles-yed-UMFdNqze.js.map → styles-yed-XeFBQ6VS.js.map} +1 -1
- package/dist/cx.js +2 -2
- package/dist/cx2.js +2 -2
- package/dist/cys.js +1 -1
- package/dist/dot.js +7 -4
- package/dist/gexf.js +2 -2
- package/dist/gml.js +8 -8
- package/dist/gml.js.map +1 -1
- package/dist/graph-io.js +129 -104
- package/dist/graph-io.js.map +1 -1
- package/dist/graphml.js +2 -2
- package/dist/json.js +1 -1
- package/dist/pajek.js +1 -1
- package/dist/sources-cypher.js +25 -22
- package/dist/sources-cypher.js.map +1 -1
- package/dist/sources-ndex.js +2 -2
- package/dist/sources-rest.js +20 -4
- package/dist/sources-rest.js.map +1 -1
- package/dist/sources-sparql.js +40 -6
- package/dist/sources-sparql.js.map +1 -1
- package/dist/src/formats/dot/exporter.d.ts.map +1 -1
- package/dist/src/formats/dot/exporter.js +34 -2
- package/dist/src/formats/dot/exporter.js.map +1 -1
- package/dist/src/formats/dot/importer.d.ts +6 -1
- package/dist/src/formats/dot/importer.d.ts.map +1 -1
- package/dist/src/formats/dot/importer.js +65 -5
- package/dist/src/formats/dot/importer.js.map +1 -1
- package/dist/src/formats/dot/index.d.ts +3 -1
- package/dist/src/formats/dot/index.d.ts.map +1 -1
- package/dist/src/formats/dot/index.js +2 -1
- package/dist/src/formats/dot/index.js.map +1 -1
- package/dist/src/formats/dot/label-places.d.ts +7 -0
- package/dist/src/formats/dot/label-places.d.ts.map +1 -1
- package/dist/src/formats/dot/label-places.js +9 -3
- package/dist/src/formats/dot/label-places.js.map +1 -1
- package/dist/src/formats/dot/names.d.ts +13 -0
- package/dist/src/formats/dot/names.d.ts.map +1 -1
- package/dist/src/formats/dot/names.js +13 -0
- package/dist/src/formats/dot/names.js.map +1 -1
- package/dist/src/formats/dot/styles.d.ts +10 -0
- package/dist/src/formats/dot/styles.d.ts.map +1 -1
- package/dist/src/formats/dot/styles.js +49 -3
- package/dist/src/formats/dot/styles.js.map +1 -1
- package/dist/src/formats/gml/styles.js +1 -1
- package/dist/src/formats/gml/styles.js.map +1 -1
- package/dist/src/formats/graphml/styles-yed.js +3 -3
- package/dist/src/formats/graphml/styles-yed.js.map +1 -1
- package/dist/src/formats/graphml/styles-yfiles3.d.ts.map +1 -1
- package/dist/src/formats/graphml/styles-yfiles3.js +6 -4
- package/dist/src/formats/graphml/styles-yfiles3.js.map +1 -1
- package/dist/src/formats/json/styles.d.ts.map +1 -1
- package/dist/src/formats/json/styles.js +94 -6
- package/dist/src/formats/json/styles.js.map +1 -1
- package/dist/src/formats/pajek/draw.d.ts +8 -0
- package/dist/src/formats/pajek/draw.d.ts.map +1 -1
- package/dist/src/formats/pajek/draw.js +10 -0
- package/dist/src/formats/pajek/draw.js.map +1 -1
- package/dist/src/formats/pajek/styles.d.ts.map +1 -1
- package/dist/src/formats/pajek/styles.js +142 -25
- package/dist/src/formats/pajek/styles.js.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/sources/credentials.d.ts +50 -1
- package/dist/src/sources/credentials.d.ts.map +1 -1
- package/dist/src/sources/credentials.js +69 -0
- package/dist/src/sources/credentials.js.map +1 -1
- package/dist/src/sources/cypher/index.d.ts.map +1 -1
- package/dist/src/sources/cypher/index.js +12 -11
- package/dist/src/sources/cypher/index.js.map +1 -1
- package/dist/src/sources/cypher/query-builder.d.ts +6 -6
- package/dist/src/sources/cypher/query-builder.d.ts.map +1 -1
- package/dist/src/sources/cypher/query-builder.js +15 -15
- package/dist/src/sources/cypher/query-builder.js.map +1 -1
- package/dist/src/sources/rest/descriptor.d.ts.map +1 -1
- package/dist/src/sources/rest/descriptor.js +4 -0
- package/dist/src/sources/rest/descriptor.js.map +1 -1
- package/dist/src/sources/rest/openalex.d.ts +4 -2
- package/dist/src/sources/rest/openalex.d.ts.map +1 -1
- package/dist/src/sources/rest/openalex.js +4 -2
- package/dist/src/sources/rest/openalex.js.map +1 -1
- package/dist/src/sources/rest/source.d.ts.map +1 -1
- package/dist/src/sources/rest/source.js +23 -0
- package/dist/src/sources/rest/source.js.map +1 -1
- package/dist/src/sources/run.d.ts +2 -0
- package/dist/src/sources/run.d.ts.map +1 -1
- package/dist/src/sources/run.js +30 -3
- package/dist/src/sources/run.js.map +1 -1
- package/dist/src/sources/sparql/index.d.ts.map +1 -1
- package/dist/src/sources/sparql/index.js +14 -1
- package/dist/src/sources/sparql/index.js.map +1 -1
- package/dist/src/sources/sparql/query.d.ts +2 -1
- package/dist/src/sources/sparql/query.d.ts.map +1 -1
- package/dist/src/sources/sparql/query.js +55 -4
- package/dist/src/sources/sparql/query.js.map +1 -1
- package/dist/src/sources/types.d.ts +29 -0
- package/dist/src/sources/types.d.ts.map +1 -1
- package/dist/src/styles/canon/enums.d.ts.map +1 -1
- package/dist/src/styles/canon/enums.js +8 -0
- package/dist/src/styles/canon/enums.js.map +1 -1
- package/dist/src/styles/cyjs/cascade.d.ts.map +1 -1
- package/dist/src/styles/cyjs/cascade.js +124 -0
- package/dist/src/styles/cyjs/cascade.js.map +1 -1
- package/dist/src/styles/cyjs/defaults-table.d.ts.map +1 -1
- package/dist/src/styles/cyjs/defaults-table.js +1 -0
- package/dist/src/styles/cyjs/defaults-table.js.map +1 -1
- package/dist/src/styles/cyjs/emit.d.ts.map +1 -1
- package/dist/src/styles/cyjs/emit.js +30 -1
- package/dist/src/styles/cyjs/emit.js.map +1 -1
- package/dist/src/styles/cyjs/mappers.d.ts.map +1 -1
- package/dist/src/styles/cyjs/mappers.js +14 -0
- package/dist/src/styles/cyjs/mappers.js.map +1 -1
- package/dist/src/styles/types.d.ts +65 -0
- package/dist/src/styles/types.d.ts.map +1 -1
- package/dist/src/styles/vocabulary.d.ts.map +1 -1
- package/dist/src/styles/vocabulary.js +21 -0
- package/dist/src/styles/vocabulary.js.map +1 -1
- package/dist/styles.js +6 -6
- package/dist/xgmml.js +3 -3
- package/package.json +2 -2
- package/src/formats/dot/exporter.ts +37 -1
- package/src/formats/dot/importer.ts +75 -4
- package/src/formats/dot/index.ts +3 -1
- package/src/formats/dot/label-places.ts +16 -3
- package/src/formats/dot/names.ts +15 -0
- package/src/formats/dot/styles.ts +55 -3
- package/src/formats/gml/styles.ts +1 -1
- package/src/formats/graphml/styles-yed.ts +3 -3
- package/src/formats/graphml/styles-yfiles3.ts +6 -4
- package/src/formats/json/styles.ts +102 -6
- package/src/formats/pajek/draw.ts +11 -0
- package/src/formats/pajek/styles.ts +151 -26
- package/src/index.ts +7 -0
- package/src/sources/credentials.ts +90 -1
- package/src/sources/cypher/index.ts +12 -11
- package/src/sources/cypher/query-builder.ts +15 -15
- package/src/sources/rest/descriptor.ts +4 -0
- package/src/sources/rest/openalex.ts +4 -2
- package/src/sources/rest/source.ts +25 -0
- package/src/sources/run.ts +35 -3
- package/src/sources/sparql/index.ts +16 -1
- package/src/sources/sparql/query.ts +58 -4
- package/src/sources/types.ts +33 -0
- package/src/styles/canon/enums.ts +9 -0
- package/src/styles/cyjs/cascade.ts +143 -0
- package/src/styles/cyjs/defaults-table.ts +1 -0
- package/src/styles/cyjs/emit.ts +35 -1
- package/src/styles/cyjs/mappers.ts +14 -0
- package/src/styles/types.ts +65 -0
- package/src/styles/vocabulary.ts +21 -0
- package/dist/chunks/block-iptrlMBb.js.map +0 -1
- package/dist/chunks/credentials-DXHx7nwR.js.map +0 -1
- package/dist/chunks/emit-BwHQkZle.js.map +0 -1
- package/dist/chunks/exporter-D2CETOMK.js.map +0 -1
- package/dist/chunks/importer-CT9mJ1nn.js.map +0 -1
- package/dist/chunks/importer-DaLTjbds.js.map +0 -1
- package/dist/chunks/importer-vRq4h3j5.js.map +0 -1
- package/dist/chunks/styles-Cm62yyQX.js.map +0 -1
|
@@ -7,7 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { failAlone } from "./fail.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type AwsCredentials,
|
|
12
|
+
type Credential,
|
|
13
|
+
type CredentialKind,
|
|
14
|
+
type SourceCredentialSlot,
|
|
15
|
+
type SourceDescriptor,
|
|
16
|
+
} from "./types.js";
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* Where a credential may go: the load's endpoint origin by default. Experimental.
|
|
@@ -145,6 +151,63 @@ function base64(text: string): string {
|
|
|
145
151
|
return btoa(binary);
|
|
146
152
|
}
|
|
147
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The plain value a slot's credential is made from: a key or a token, or a user name and password.
|
|
156
|
+
* @category Network sources
|
|
157
|
+
*/
|
|
158
|
+
export type SlotValue =
|
|
159
|
+
| string
|
|
160
|
+
| {
|
|
161
|
+
/** A basic slot's user name. */
|
|
162
|
+
readonly username: string;
|
|
163
|
+
/** A basic slot's password. */
|
|
164
|
+
readonly password: string;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The credential a slot describes, from its plain value.
|
|
169
|
+
* @param slot - the slot
|
|
170
|
+
* @param value - the key or token, or a basic slot's user name and password
|
|
171
|
+
* @param bind - the origins it may be sent to
|
|
172
|
+
* @returns the credential
|
|
173
|
+
*/
|
|
174
|
+
function forSlot(slot: SourceCredentialSlot, value: SlotValue, bind?: CredentialBind): Credential {
|
|
175
|
+
if (typeof value !== "string") {
|
|
176
|
+
if (slot.kind === "basic") {
|
|
177
|
+
return new SourceCredential({ kind: "basic", username: value.username, password: value.password }, bind);
|
|
178
|
+
}
|
|
179
|
+
} else if (slot.kind === "bearer") {
|
|
180
|
+
return new SourceCredential({ kind: "bearer", token: value }, bind);
|
|
181
|
+
} else if (slot.kind === "api-key") {
|
|
182
|
+
const send = slot.send ?? { query: slot.name };
|
|
183
|
+
const header = "header" in send ? send.header : null;
|
|
184
|
+
const query = "query" in send ? send.query : null;
|
|
185
|
+
return new SourceCredential({ kind: "api-key", header, query, value }, bind);
|
|
186
|
+
}
|
|
187
|
+
return failAlone("E_SOURCE_PARAM", `a ${slot.kind} slot cannot take this value`, {
|
|
188
|
+
name: "credential",
|
|
189
|
+
reason: "type",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The credential slots a source takes: the ones its descriptor declares, else one slot `default`
|
|
195
|
+
* of its first credential kind when it takes a credential (`auth`), else none. Experimental.
|
|
196
|
+
* @param descriptor - the source's descriptor
|
|
197
|
+
* @returns the slots
|
|
198
|
+
* @category Network sources
|
|
199
|
+
*/
|
|
200
|
+
export function credentialSlots(descriptor: SourceDescriptor | undefined): readonly SourceCredentialSlot[] {
|
|
201
|
+
if (descriptor?.credentials !== undefined) {
|
|
202
|
+
return descriptor.credentials;
|
|
203
|
+
}
|
|
204
|
+
const kind = descriptor?.auth?.[0];
|
|
205
|
+
if (kind === undefined || descriptor?.authRequired === "none") {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
return [{ name: "default", kind, required: descriptor?.authRequired === "required" }];
|
|
209
|
+
}
|
|
210
|
+
|
|
148
211
|
/**
|
|
149
212
|
* The factories that make credentials. Pass the result as the `credential` option of a load; it is
|
|
150
213
|
* sent only to the load's endpoint origin, or to `bind.origins` when you give them. Experimental.
|
|
@@ -213,6 +276,21 @@ export const credentials = Object.freeze({
|
|
|
213
276
|
custom(apply: (request: Request) => Promise<Request> | Request, kind: string, bind?: CredentialBind): Credential {
|
|
214
277
|
return new SourceCredential({ kind: "custom", name: kind, apply }, bind);
|
|
215
278
|
},
|
|
279
|
+
/**
|
|
280
|
+
* The credential a source's slot describes (`credentialSlots(descriptor)`), from its plain
|
|
281
|
+
* value: an API key sent as the slot says, a bearer token, or a basic slot's user name and
|
|
282
|
+
* password.
|
|
283
|
+
* @example
|
|
284
|
+
* ```ts
|
|
285
|
+
* const [slot] = credentialSlots(openalex.descriptor); // api_key, sent as ?api_key=
|
|
286
|
+
* const credential = credentials.forSlot(slot, key);
|
|
287
|
+
* ```
|
|
288
|
+
* @param slot - the slot
|
|
289
|
+
* @param value - the key or token, or `{ username, password }` for a basic slot
|
|
290
|
+
* @param bind - the origins it may be sent to
|
|
291
|
+
* @returns the credential
|
|
292
|
+
*/
|
|
293
|
+
forSlot,
|
|
216
294
|
});
|
|
217
295
|
|
|
218
296
|
/**
|
|
@@ -248,6 +326,17 @@ export function credentialHeader(credential: Credential): string | null {
|
|
|
248
326
|
: null;
|
|
249
327
|
}
|
|
250
328
|
|
|
329
|
+
/**
|
|
330
|
+
* The query parameter a credential is sent in, so a cursor can leave it out; null for one sent in
|
|
331
|
+
* a header or not at all.
|
|
332
|
+
* @param credential - the credential
|
|
333
|
+
* @returns the parameter's name
|
|
334
|
+
*/
|
|
335
|
+
export function credentialQuery(credential: Credential): string | null {
|
|
336
|
+
const secret = secretOf(credential);
|
|
337
|
+
return secret?.kind === "api-key" && secret.header === null ? (secret.query ?? "api_key") : null;
|
|
338
|
+
}
|
|
339
|
+
|
|
251
340
|
/**
|
|
252
341
|
* Add a credential to a request. Internal to graph-io: only `ctx.fetch` calls it.
|
|
253
342
|
* @param credential - the credential
|
|
@@ -253,7 +253,8 @@ class Reader {
|
|
|
253
253
|
* Mode "query": the reader's own query, as written. With a node cap (or a cursor from a capped
|
|
254
254
|
* run) the server returns at most that many rows (`CALL { query } RETURN * LIMIT n`), and a full
|
|
255
255
|
* result hands back a cursor that runs the query again with twice the rows, for the caller to
|
|
256
|
-
* merge. A server that refuses the wrapper
|
|
256
|
+
* merge. A server that refuses the wrapper fails the load with `E_SOURCE_PARAM` (`name:
|
|
257
|
+
* "maxNodes"`, `reason: "uncappable"`): the query is never run past the cap the caller set.
|
|
257
258
|
* @param query - the query
|
|
258
259
|
* @param cursor - where to resume: `{ rows }` from an earlier capped run
|
|
259
260
|
* @returns the one page
|
|
@@ -267,7 +268,6 @@ class Reader {
|
|
|
267
268
|
? null
|
|
268
269
|
: Number(checkedInteger(asked, "maxNodes", 1, Number.MAX_SAFE_INTEGER, ctx.report));
|
|
269
270
|
let result: QueryResult;
|
|
270
|
-
let capped = rows !== null;
|
|
271
271
|
try {
|
|
272
272
|
result = await this.transport.run(
|
|
273
273
|
rows === null
|
|
@@ -276,16 +276,18 @@ class Reader {
|
|
|
276
276
|
parameters,
|
|
277
277
|
);
|
|
278
278
|
} catch (err) {
|
|
279
|
-
if (
|
|
279
|
+
if (rows === null || !(err instanceof ImportError) || err.issue?.code !== SOURCE_ISSUE.E_SOURCE_QUERY) {
|
|
280
280
|
throw err;
|
|
281
281
|
}
|
|
282
|
-
|
|
283
|
-
|
|
282
|
+
return failOn(ctx.report, "E_SOURCE_PARAM", "the server cannot cap this query; load it without maxNodes", {
|
|
283
|
+
name: "maxNodes",
|
|
284
|
+
reason: "uncappable",
|
|
285
|
+
});
|
|
284
286
|
}
|
|
285
287
|
const collected = new Collector();
|
|
286
288
|
collected.add(result, false);
|
|
287
289
|
requireGraph(result, collected, ctx.report);
|
|
288
|
-
const full =
|
|
290
|
+
const full = rows !== null && result.rows.length >= rows;
|
|
289
291
|
return { collected, next: full ? { rows: rows * 2 } : null };
|
|
290
292
|
}
|
|
291
293
|
|
|
@@ -362,13 +364,12 @@ class Reader {
|
|
|
362
364
|
(d) => nodePage(profile, this.filters, String(size), after !== null, d),
|
|
363
365
|
after === null ? {} : { after },
|
|
364
366
|
);
|
|
365
|
-
const
|
|
367
|
+
const at = fields.indexOf("key");
|
|
368
|
+
const last = rows.at(-1)?.[at];
|
|
366
369
|
const key = typeof last === "string" || typeof last === "number" ? last : null;
|
|
367
370
|
if (key !== null) {
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
...(after === null ? {} : { after }),
|
|
371
|
-
});
|
|
371
|
+
const keys = rows.map((row) => row[at]);
|
|
372
|
+
const among = await this.transport.run(edgesAmong(profile, this.filters), { keys, last: key });
|
|
372
373
|
collected.add(among, false);
|
|
373
374
|
}
|
|
374
375
|
const next = rows.length >= size && key !== null ? { phase: "nodes", after: key } : null;
|
|
@@ -216,25 +216,25 @@ export function edgePage(profile: CypherProfile, filters: QueryFilters, pageSize
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
/**
|
|
219
|
-
* The relationships among the nodes a capped mode "all" has loaded
|
|
220
|
-
*
|
|
221
|
-
*
|
|
219
|
+
* The relationships among the nodes a capped mode "all" has loaded that touch one node page: each
|
|
220
|
+
* starts from the page's own nodes `$keys` (an id or index lookup, never a scan of every
|
|
221
|
+
* relationship), with its other end on this page or an earlier one (at or before `$last`), so each
|
|
222
|
+
* relationship arrives once, with the page that loads its later end.
|
|
222
223
|
* @param profile - the server's profile
|
|
223
224
|
* @param filters - the filters
|
|
224
|
-
* @
|
|
225
|
-
* @returns the statement; it reads `$last` and `$after`
|
|
225
|
+
* @returns the statement; it reads `$keys` and `$last`
|
|
226
226
|
*/
|
|
227
|
-
export function edgesAmong(profile: CypherProfile, filters: QueryFilters
|
|
228
|
-
const
|
|
227
|
+
export function edgesAmong(profile: CypherProfile, filters: QueryFilters): string {
|
|
228
|
+
const ka = idExpression("a", profile, filters);
|
|
229
|
+
const kb = idExpression("b", profile, filters);
|
|
230
|
+
const ends = [nodePredicate("a", filters), nodePredicate("b", filters)].filter((p) => p !== null);
|
|
231
|
+
const rest = ends.length === 0 ? "" : ` AND ${ends.join(" AND ")}`;
|
|
232
|
+
const pattern = `MATCH (a)-[r${typesPattern(filters)}]->(b)`;
|
|
229
233
|
return [
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
"RETURN a, r, b",
|
|
235
|
-
]
|
|
236
|
-
.filter((l) => l !== null)
|
|
237
|
-
.join(" ");
|
|
234
|
+
`${pattern} WHERE ${ka} IN $keys AND ${kb} <= $last${rest} RETURN a, r, b`,
|
|
235
|
+
"UNION ALL",
|
|
236
|
+
`${pattern} WHERE ${kb} IN $keys AND ${ka} <= $last AND NOT ${ka} IN $keys${rest} RETURN a, r, b`,
|
|
237
|
+
].join(" ");
|
|
238
238
|
}
|
|
239
239
|
|
|
240
240
|
/** How far a neighborhood reaches. */
|
|
@@ -49,6 +49,10 @@ export const OPENALEX_DESCRIPTOR: SourceDescriptor = Object.freeze({
|
|
|
49
49
|
defaultEndpoint: "https://api.openalex.org",
|
|
50
50
|
auth: ["api-key"],
|
|
51
51
|
authRequired: "optional",
|
|
52
|
+
// OpenAlex documents its key as the api_key query parameter
|
|
53
|
+
credentials: [
|
|
54
|
+
{ name: "api_key", kind: "api-key", required: false, plainName: "API key", send: { query: "api_key" } },
|
|
55
|
+
],
|
|
52
56
|
apiKey: { query: "api_key" },
|
|
53
57
|
rateLimit: { requests: 10, perMs: 1000 },
|
|
54
58
|
features: ["expand", "probe", "validate", "page"],
|
|
@@ -266,8 +266,10 @@ const methods = restMethods<OpenAlexParams>(plan, false);
|
|
|
266
266
|
* The OpenAlex preset (https://openalex.org): `work` (the citation graph around a work, default)
|
|
267
267
|
* or `author` (the works they wrote), `graph: "coauthorship"` for authors joined by shared works,
|
|
268
268
|
* and `include` (`"authors"`, `"institutions"`, `"topics"`) for more node kinds. Without a key,
|
|
269
|
-
* the first page loads and a later one warns W_SOURCE_AUTH with `report.source.next
|
|
270
|
-
*
|
|
269
|
+
* the first page loads and a later one warns W_SOURCE_AUTH with `report.source.next`. Its one
|
|
270
|
+
* optional credential slot, `api_key`, is sent as the `api_key` query parameter: pass
|
|
271
|
+
* `credentials.forSlot(slot, key)` (or `credentials.apiKey({ query: "api_key" }, key)`) and
|
|
272
|
+
* continue with the cursor. Experimental.
|
|
271
273
|
* @category Network sources
|
|
272
274
|
*/
|
|
273
275
|
export const openalexSource: GraphSource<OpenAlexParams> = defineSource<OpenAlexParams>({
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type GraphSource,
|
|
20
20
|
type RecordBatch,
|
|
21
21
|
type SourceContext,
|
|
22
|
+
type SourceCredentialSlot,
|
|
22
23
|
type SourceCursor,
|
|
23
24
|
type SourceDescriptor,
|
|
24
25
|
type SourceFinish,
|
|
@@ -697,6 +698,29 @@ export type RestSourceInfo = {
|
|
|
697
698
|
readonly id: string;
|
|
698
699
|
} & Partial<SourceDescriptor>;
|
|
699
700
|
|
|
701
|
+
/**
|
|
702
|
+
* The credential slot a mapping's `auth` describes: an API key's slot is named by the query
|
|
703
|
+
* parameter or header it is sent in (`{ query: "api_key" }` is the slot `api_key`), a bearer
|
|
704
|
+
* token's is `default`. Optional, as a mapping cannot say a key is required.
|
|
705
|
+
* @param mapping - the mapping
|
|
706
|
+
* @returns the slots
|
|
707
|
+
*/
|
|
708
|
+
function mappingSlots(mapping: RestMapping): readonly SourceCredentialSlot[] {
|
|
709
|
+
const { auth } = mapping;
|
|
710
|
+
if (auth === undefined) {
|
|
711
|
+
return [];
|
|
712
|
+
}
|
|
713
|
+
if (auth.kind === "bearer") {
|
|
714
|
+
return [{ name: "default", kind: "bearer", required: false }];
|
|
715
|
+
}
|
|
716
|
+
const { header } = auth;
|
|
717
|
+
if (header !== undefined) {
|
|
718
|
+
return [{ name: header.toLowerCase(), kind: "api-key", required: false, send: { header } }];
|
|
719
|
+
}
|
|
720
|
+
const query = auth.query ?? "api_key";
|
|
721
|
+
return [{ name: query, kind: "api-key", required: false, send: { query } }];
|
|
722
|
+
}
|
|
723
|
+
|
|
700
724
|
/**
|
|
701
725
|
* Where a mapping's API key goes, as a descriptor says it.
|
|
702
726
|
* @param auth - the mapping's auth
|
|
@@ -748,6 +772,7 @@ export function restSource(mapping: RestMapping, info: RestSourceInfo): GraphSou
|
|
|
748
772
|
defaultEndpoint: mapping.endpoint,
|
|
749
773
|
auth,
|
|
750
774
|
authRequired: auth.length === 0 ? "none" : "optional",
|
|
775
|
+
credentials: mappingSlots(mapping),
|
|
751
776
|
...apiKeyOf(mapping.auth),
|
|
752
777
|
...(mapping.rateLimit === undefined ? {} : { rateLimit: mapping.rateLimit }),
|
|
753
778
|
...fields,
|
package/src/sources/run.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { type FormatRegistry } from "../registry.js";
|
|
|
15
15
|
import { SNIFF_HEAD_BYTES } from "../sniff.js";
|
|
16
16
|
import { type CommonImportOptions, ImportError, type ImportInput, type ImportReport } from "../types.js";
|
|
17
17
|
import { type CreatedContext, createSourceContext, detectRuntime, LimitStop } from "./context.js";
|
|
18
|
-
import { originOf } from "./credentials.js";
|
|
18
|
+
import { credentialQuery, originOf } from "./credentials.js";
|
|
19
19
|
import { checkParams } from "./define.js";
|
|
20
20
|
import { failOn, warnOn } from "./fail.js";
|
|
21
21
|
import { SOURCE_FORMAT_PREFIX } from "./ids.js";
|
|
@@ -60,6 +60,8 @@ interface Prepared {
|
|
|
60
60
|
readonly params: Record<string, unknown>;
|
|
61
61
|
/** The sink wrapper's counters, set once the sink is known. */
|
|
62
62
|
readonly counts: { nodes: Set<number>; edgesAtStart: number; sink: GraphSink | null };
|
|
63
|
+
/** The query parameter the call's credential is sent in, which a cursor never keeps; null for none. */
|
|
64
|
+
readonly secretQuery: string | null;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/**
|
|
@@ -131,7 +133,8 @@ function prepare(
|
|
|
131
133
|
edges: counts.sink === null ? 0 : counts.sink.edgeCount - counts.edgesAtStart,
|
|
132
134
|
}),
|
|
133
135
|
});
|
|
134
|
-
|
|
136
|
+
const secretQuery = options.credential === undefined ? null : credentialQuery(options.credential);
|
|
137
|
+
return { report, created, params, counts, secretQuery };
|
|
135
138
|
}
|
|
136
139
|
|
|
137
140
|
/**
|
|
@@ -721,6 +724,7 @@ export async function runMethod<T>(
|
|
|
721
724
|
*/
|
|
722
725
|
export function withSource(report: ImportReport, prepared: Prepared, next: SourceCursor | null): ImportReport {
|
|
723
726
|
const own = prepared.report.finish();
|
|
727
|
+
const cursor = prepared.secretQuery === null || next === null ? next : withoutParam(next, prepared.secretQuery);
|
|
724
728
|
// issues the context recorded (retries, limits) that a source's own report does not hold
|
|
725
729
|
const missing = own.issues.filter((i) => !report.issues.includes(i));
|
|
726
730
|
return Object.freeze({
|
|
@@ -728,10 +732,38 @@ export function withSource(report: ImportReport, prepared: Prepared, next: Sourc
|
|
|
728
732
|
issues: missing.length === 0 ? report.issues : Object.freeze([...report.issues, ...missing]),
|
|
729
733
|
warningCount: report.warningCount + missing.filter((i) => i.severity === "warning").length,
|
|
730
734
|
errorCount: report.errorCount + missing.filter((i) => i.severity === "error").length,
|
|
731
|
-
source: prepared.created.info(
|
|
735
|
+
source: prepared.created.info(cursor),
|
|
732
736
|
});
|
|
733
737
|
}
|
|
734
738
|
|
|
739
|
+
/**
|
|
740
|
+
* A cursor without a query parameter in any URL it holds: a next-page link a service returns
|
|
741
|
+
* repeats the API key the request carried, and a cursor is saved where a credential never is.
|
|
742
|
+
* The credential is sent again with every request, so the next page does not need it there.
|
|
743
|
+
* @param value - the cursor, or a value inside it
|
|
744
|
+
* @param name - the parameter
|
|
745
|
+
* @returns the value without it
|
|
746
|
+
*/
|
|
747
|
+
function withoutParam<T>(value: T, name: string): T {
|
|
748
|
+
if (typeof value === "string" && /^https?:\/\//i.test(value)) {
|
|
749
|
+
const url = new URL(value);
|
|
750
|
+
if (!url.searchParams.has(name)) {
|
|
751
|
+
return value;
|
|
752
|
+
}
|
|
753
|
+
url.searchParams.delete(name);
|
|
754
|
+
return url.href as T;
|
|
755
|
+
}
|
|
756
|
+
if (Array.isArray(value)) {
|
|
757
|
+
return Object.freeze(value.map((each: unknown) => withoutParam(each, name))) as T;
|
|
758
|
+
}
|
|
759
|
+
if (typeof value === "object" && value !== null) {
|
|
760
|
+
return Object.freeze(
|
|
761
|
+
Object.fromEntries(Object.entries(value).map(([key, each]) => [key, withoutParam(each, name)])),
|
|
762
|
+
) as T;
|
|
763
|
+
}
|
|
764
|
+
return value;
|
|
765
|
+
}
|
|
766
|
+
|
|
735
767
|
/**
|
|
736
768
|
* What a failed call throws: the signal's reason when it aborted, an ImportError with the source
|
|
737
769
|
* info attached, or the error itself.
|
|
@@ -230,13 +230,28 @@ function write(graphed: Graphed, sink: GraphSink, ctx: SourceContext): void {
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Order strings by UTF-16 code units, as `Array.prototype.sort` does with no compare function, so
|
|
235
|
+
* a content hash never depends on the locale.
|
|
236
|
+
* @param a - one string
|
|
237
|
+
* @param b - the other
|
|
238
|
+
* @returns negative, zero or positive
|
|
239
|
+
*/
|
|
240
|
+
function byCodeUnits(a: string, b: string): number {
|
|
241
|
+
if (a === b) {
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return a < b ? -1 : 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
233
248
|
/**
|
|
234
249
|
* The content hash of some triples, in any order.
|
|
235
250
|
* @param triples - the triples
|
|
236
251
|
* @returns the SHA-256, hex
|
|
237
252
|
*/
|
|
238
253
|
function hashOf(triples: readonly Triple[]): Promise<string> {
|
|
239
|
-
return sha256Hex(triples.map(canonicalTriple).sort().join("\n"));
|
|
254
|
+
return sha256Hex(triples.map(canonicalTriple).sort(byCodeUnits).join("\n"));
|
|
240
255
|
}
|
|
241
256
|
|
|
242
257
|
/**
|
|
@@ -37,19 +37,73 @@ function bare(query: string): string {
|
|
|
37
37
|
.replaceAll(/#[^\n]*/g, "");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** The keywords of SPARQL Update, which a query this source sends never holds. */
|
|
41
|
+
const UPDATE_KEYWORDS = /\b(INSERT|DELETE|LOAD|CLEAR|DROP|CREATE|ADD|MOVE|COPY|WITH)\b/i;
|
|
42
|
+
|
|
43
|
+
/** One declaration of the prologue, as `bare` leaves it: `BASE <>` or `PREFIX ex: <>`. */
|
|
44
|
+
const DECLARATION = /^\s*(?:BASE|PREFIX\s*[^\s:<]*:)\s*<>/i;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Bare text after its prologue, one declaration at a time.
|
|
48
|
+
* @param text - the bare text
|
|
49
|
+
* @returns what follows the prologue
|
|
50
|
+
*/
|
|
51
|
+
function afterPrologue(text: string): string {
|
|
52
|
+
let rest = text;
|
|
53
|
+
for (let found = DECLARATION.exec(rest); found !== null; found = DECLARATION.exec(rest)) {
|
|
54
|
+
rest = rest.slice(found[0].length);
|
|
55
|
+
}
|
|
56
|
+
return rest;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The bare text with variables and prefixed names blanked too, so a name such as `ex:LIMIT` or
|
|
61
|
+
* `?DELETE` is never read as a keyword.
|
|
62
|
+
* @param text - the bare text after the prologue
|
|
63
|
+
* @returns the text holding only keywords, punctuation and blanks
|
|
64
|
+
*/
|
|
65
|
+
function keywordsOnly(text: string): string {
|
|
66
|
+
// one run of name characters at a time: a run holding a colon is a prefixed name
|
|
67
|
+
return text.replaceAll(/[?$]\w+/g, "?v").replaceAll(/[\w.%:-]+/g, (run) => (run.includes(":") ? "_" : run));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Whether bare text holds a `;` outside every brace: SPARQL Update separates its operations so, and
|
|
72
|
+
* a query never holds one there.
|
|
73
|
+
* @param text - the text
|
|
74
|
+
* @returns true when it does
|
|
75
|
+
*/
|
|
76
|
+
function semicolonOutside(text: string): boolean {
|
|
77
|
+
let depth = 0;
|
|
78
|
+
for (const char of text) {
|
|
79
|
+
if (char === "{") {
|
|
80
|
+
depth++;
|
|
81
|
+
} else if (char === "}") {
|
|
82
|
+
depth--;
|
|
83
|
+
} else if (char === ";" && depth <= 0) {
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
40
90
|
/**
|
|
41
|
-
* Read what paging a caller's query takes.
|
|
91
|
+
* Read what paging a caller's query takes. Its form is the first keyword after the prologue; a
|
|
92
|
+
* text that holds an update keyword, or a `;` outside braces, has no form, so it is refused.
|
|
42
93
|
* @param query - the query
|
|
43
94
|
* @returns its form, its own modifiers and its template variables
|
|
44
95
|
*/
|
|
45
96
|
export function readQuery(query: string): UserQuery {
|
|
46
|
-
const
|
|
47
|
-
const
|
|
97
|
+
const body = afterPrologue(bare(query));
|
|
98
|
+
const text = keywordsOnly(body);
|
|
99
|
+
const first = /^\s*([A-Za-z]+)/.exec(text)?.[1]?.toUpperCase() ?? "";
|
|
100
|
+
const update = UPDATE_KEYWORDS.test(text) || semicolonOutside(text);
|
|
101
|
+
const form = !update && ["CONSTRUCT", "DESCRIBE", "SELECT", "ASK"].includes(first) ? first.toLowerCase() : null;
|
|
48
102
|
// the solution modifiers follow the last closing brace (a trailing VALUES block is not handled)
|
|
49
103
|
const tail = text.slice(text.lastIndexOf("}") + 1);
|
|
50
104
|
let templateVars: string[] = [];
|
|
51
105
|
if (form === "construct") {
|
|
52
|
-
const template = /\bCONSTRUCT\s*\{([^}]*)\}/i.exec(
|
|
106
|
+
const template = /\bCONSTRUCT\s*\{([^}]*)\}/i.exec(body)?.[1] ?? "";
|
|
53
107
|
templateVars = [...new Set(template.match(/[?$]\w+/g) ?? [])].map((v) => `?${v.slice(1)}`);
|
|
54
108
|
}
|
|
55
109
|
return {
|
package/src/sources/types.ts
CHANGED
|
@@ -378,6 +378,34 @@ export interface SourceSample {
|
|
|
378
378
|
readonly firstPage?: number | undefined;
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
/**
|
|
382
|
+
* One credential a source takes, as its descriptor declares it: a name a project file records
|
|
383
|
+
* instead of the credential, the kind, whether a load needs it, and for an API key how it is sent.
|
|
384
|
+
* `credentials.forSlot(slot, value)` makes the credential it describes. Experimental.
|
|
385
|
+
* @category Network sources
|
|
386
|
+
*/
|
|
387
|
+
export interface SourceCredentialSlot {
|
|
388
|
+
/** The slot's name, recorded as a load's `credentialSlot` ("api_key", "default"). */
|
|
389
|
+
readonly name: string;
|
|
390
|
+
/** The credential kind it takes. */
|
|
391
|
+
readonly kind: CredentialKind;
|
|
392
|
+
/** Whether a load needs it. @defaultValue false */
|
|
393
|
+
readonly required?: boolean | undefined;
|
|
394
|
+
/** Its default name; catalog data an application may replace by name. */
|
|
395
|
+
readonly plainName?: string | undefined;
|
|
396
|
+
/** For an API key: the query parameter or request header it is sent in. */
|
|
397
|
+
readonly send?:
|
|
398
|
+
| {
|
|
399
|
+
/** The query parameter the key is sent in. */
|
|
400
|
+
readonly query: string;
|
|
401
|
+
}
|
|
402
|
+
| {
|
|
403
|
+
/** The request header the key is sent in. */
|
|
404
|
+
readonly header: string;
|
|
405
|
+
}
|
|
406
|
+
| undefined;
|
|
407
|
+
}
|
|
408
|
+
|
|
381
409
|
/**
|
|
382
410
|
* What a form needs to ask for a source's parameters, as data. Only `plainName` and `params` are
|
|
383
411
|
* required. Experimental.
|
|
@@ -396,6 +424,11 @@ export interface SourceDescriptor {
|
|
|
396
424
|
readonly auth?: readonly CredentialKind[] | undefined;
|
|
397
425
|
/** Whether a credential is needed. @defaultValue "none" */
|
|
398
426
|
readonly authRequired?: "required" | "optional" | "none" | undefined;
|
|
427
|
+
/**
|
|
428
|
+
* The credential slots it takes. A source that takes a credential (`auth`) and declares none
|
|
429
|
+
* has one slot, `default`: `credentialSlots(descriptor)` reads either. @defaultValue []
|
|
430
|
+
*/
|
|
431
|
+
readonly credentials?: readonly SourceCredentialSlot[] | undefined;
|
|
399
432
|
/**
|
|
400
433
|
* Where an `api-key` credential goes, for a form that asks for the key alone: pass it to
|
|
401
434
|
* `credentials.apiKey(descriptor.apiKey, key)`. Absent when the caller must say.
|
|
@@ -231,11 +231,20 @@ const GRAPHVIZ: Partial<Record<StyleEnumKind, Aliases>> = {
|
|
|
231
231
|
arrow: { "other:curve": ["curve"], "other:inv-curve": ["icurve"] },
|
|
232
232
|
};
|
|
233
233
|
|
|
234
|
+
/**
|
|
235
|
+
* yEd's (yFiles for Java's) STANDARD arrow is "a triangle with impressed bottom side", a notched arrow; its DELTA is the
|
|
236
|
+
* plain triangle (https://docs.yworks.com/yfiles/doc/api/y/view/Arrow.html).
|
|
237
|
+
*/
|
|
238
|
+
const YED: Partial<Record<StyleEnumKind, Aliases>> = {
|
|
239
|
+
arrow: { vee: ["standard"] },
|
|
240
|
+
};
|
|
241
|
+
|
|
234
242
|
/** Per-dialect overrides; a dialect not listed reads the common table alone. */
|
|
235
243
|
const OVERRIDES: ReadonlyMap<string, Partial<Record<StyleEnumKind, Aliases>>> = new Map([
|
|
236
244
|
["vis", VIS],
|
|
237
245
|
["graphviz", GRAPHVIZ],
|
|
238
246
|
["cytoscape-js", CYJS],
|
|
247
|
+
["yed", YED],
|
|
239
248
|
]);
|
|
240
249
|
|
|
241
250
|
const OPEN: ReadonlySet<StyleEnumKind> = new Set<StyleEnumKind>(["shape", "borderStyle", "lineStyle", "arrow"]);
|