keepset 0.20.0 → 0.21.0

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.
@@ -176,6 +176,9 @@ declare const RecipeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
176
176
  paging: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
177
177
  kind: z.ZodLiteral<"arcgis-query">;
178
178
  orderBy: z.ZodString;
179
+ }, z.core.$strict>, z.ZodObject<{
180
+ kind: z.ZodLiteral<"socrata-resource">;
181
+ pageSize: z.ZodInt;
179
182
  }, z.core.$strict>], "kind">>;
180
183
  archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
181
184
  kind: z.ZodLiteral<"none">;
@@ -332,6 +335,9 @@ declare const EntrySchema: z.ZodObject<{
332
335
  paging: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
333
336
  kind: z.ZodLiteral<"arcgis-query">;
334
337
  orderBy: z.ZodString;
338
+ }, z.core.$strict>, z.ZodObject<{
339
+ kind: z.ZodLiteral<"socrata-resource">;
340
+ pageSize: z.ZodInt;
335
341
  }, z.core.$strict>], "kind">>;
336
342
  archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
337
343
  kind: z.ZodLiteral<"none">;
@@ -541,6 +547,9 @@ declare const EntrySchema: z.ZodObject<{
541
547
  paging?: {
542
548
  kind: "arcgis-query";
543
549
  orderBy: string;
550
+ } | {
551
+ kind: "socrata-resource";
552
+ pageSize: number;
544
553
  } | undefined;
545
554
  } | {
546
555
  status: "not-fetchable";
@@ -695,6 +704,9 @@ declare const CatalogueIndexSchema: z.ZodObject<{
695
704
  paging: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
696
705
  kind: z.ZodLiteral<"arcgis-query">;
697
706
  orderBy: z.ZodString;
707
+ }, z.core.$strict>, z.ZodObject<{
708
+ kind: z.ZodLiteral<"socrata-resource">;
709
+ pageSize: z.ZodInt;
698
710
  }, z.core.$strict>], "kind">>;
699
711
  archive: z.ZodDiscriminatedUnion<[z.ZodObject<{
700
712
  kind: z.ZodLiteral<"none">;
@@ -904,6 +916,9 @@ declare const CatalogueIndexSchema: z.ZodObject<{
904
916
  paging?: {
905
917
  kind: "arcgis-query";
906
918
  orderBy: string;
919
+ } | {
920
+ kind: "socrata-resource";
921
+ pageSize: number;
907
922
  } | undefined;
908
923
  } | {
909
924
  status: "not-fetchable";
@@ -1039,6 +1054,9 @@ declare const CatalogueIndexSchema: z.ZodObject<{
1039
1054
  paging?: {
1040
1055
  kind: "arcgis-query";
1041
1056
  orderBy: string;
1057
+ } | {
1058
+ kind: "socrata-resource";
1059
+ pageSize: number;
1042
1060
  } | undefined;
1043
1061
  } | {
1044
1062
  status: "not-fetchable";
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as Parser, A as LineageSchema, B as Citation, C as CatalogueIndex, D as NamespaceNode, E as DatasetQuery, F as AliasSchema, G as Entry, H as ColumnNameSchema, I as Archive, J as IdSchema, K as EntrySchema, L as ArchiveMember, M as ManifestSchema, N as SemVerSchema, O as createCatalogue, P as parseManifest, Q as NamespaceSchema, R as ArchiveMemberSchema, S as Catalogue, T as Dataset, U as CrosswalkReference, V as CitationSchema, W as CrosswalkReferenceSchema, X as LicenceSchema, Y as Licence, Z as Namespace, _ as GraphEdgeSchema, a as CategoryTargets, at as Snapshot, b as GraphV1, c as getCategory, ct as SourceSchema, d as listCategories, dt as waybackUrl, et as ParserSchema, f as OrNode, g as GraphEdge, h as OrV1Schema, i as CategoryTarget, it as RelativePathSchema, j as Manifest, k as Lineage, l as getTarget, lt as WaybackTimestampSchema, m as OrV1, n as CategoryId, nt as Recipe, o as CountedKey, ot as SnapshotSchema, p as OrNodeSchema, q as HttpUrlSchema, r as CategoryIdSchema, rt as RecipeSchema, s as countTarget, st as Source, t as Category, tt as RESERVED_META_KEYS, u as kindOf, ut as parseEntry, v as GraphNode, w as CatalogueIndexSchema, x as GraphV1Schema, y as GraphNodeSchema, z as ArchiveSchema } from "./categories-BVROO4tM.js";
1
+ import { $ as Parser, A as LineageSchema, B as Citation, C as CatalogueIndex, D as NamespaceNode, E as DatasetQuery, F as AliasSchema, G as Entry, H as ColumnNameSchema, I as Archive, J as IdSchema, K as EntrySchema, L as ArchiveMember, M as ManifestSchema, N as SemVerSchema, O as createCatalogue, P as parseManifest, Q as NamespaceSchema, R as ArchiveMemberSchema, S as Catalogue, T as Dataset, U as CrosswalkReference, V as CitationSchema, W as CrosswalkReferenceSchema, X as LicenceSchema, Y as Licence, Z as Namespace, _ as GraphEdgeSchema, a as CategoryTargets, at as Snapshot, b as GraphV1, c as getCategory, ct as SourceSchema, d as listCategories, dt as waybackUrl, et as ParserSchema, f as OrNode, g as GraphEdge, h as OrV1Schema, i as CategoryTarget, it as RelativePathSchema, j as Manifest, k as Lineage, l as getTarget, lt as WaybackTimestampSchema, m as OrV1, n as CategoryId, nt as Recipe, o as CountedKey, ot as SnapshotSchema, p as OrNodeSchema, q as HttpUrlSchema, r as CategoryIdSchema, rt as RecipeSchema, s as countTarget, st as Source, t as Category, tt as RESERVED_META_KEYS, u as kindOf, ut as parseEntry, v as GraphNode, w as CatalogueIndexSchema, x as GraphV1Schema, y as GraphNodeSchema, z as ArchiveSchema } from "./categories-frr8n_SI.js";
2
2
  import { Crosswalk } from "twindex";
3
3
  import { z } from "zod";
4
4
  //#region src/errors.d.ts
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as NativeParseError, A as toIssues, B as kindOf, C as RESERVED_META_KEYS, D as SourceSchema, E as SnapshotSchema, G as GraphNodeSchema, H as OrNodeSchema, I as CategoryIdSchema, J as ArchiveError, K as GraphV1Schema, L as countTarget, M as NAME_SEPARATOR, N as groupsOf, O as WaybackTimestampSchema, P as namesOf, Q as KeepsetError, R as getCategory, S as ParserSchema, T as RelativePathSchema, U as OrV1Schema, V as listCategories, W as GraphEdgeSchema, X as DatasetAmbiguousError, Y as CatalogueSyntaxError, Z as DatasetNotFoundError, _ as EntrySchema, a as CatalogueIndexSchema, b as LicenceSchema, c as ManifestSchema, d as AliasSchema, et as RecipeError, f as ArchiveMemberSchema, g as CrosswalkReferenceSchema, h as ColumnNameSchema, i as listNamespaces, j as waybackUrl, k as parseEntry, l as SemVerSchema, m as CitationSchema, n as getManifest, o as createCatalogue, p as ArchiveSchema, q as JsonScalarSchema, r as listDatasets, s as LineageSchema, t as getDataset, tt as UnknownCategoryError, u as parseManifest, v as HttpUrlSchema, w as RecipeSchema, x as NamespaceSchema, y as IdSchema, z as getTarget } from "./bundled-BmqpHjOH.js";
1
+ import { $ as NativeParseError, A as toIssues, B as kindOf, C as RESERVED_META_KEYS, D as SourceSchema, E as SnapshotSchema, G as GraphNodeSchema, H as OrNodeSchema, I as CategoryIdSchema, J as ArchiveError, K as GraphV1Schema, L as countTarget, M as NAME_SEPARATOR, N as groupsOf, O as WaybackTimestampSchema, P as namesOf, Q as KeepsetError, R as getCategory, S as ParserSchema, T as RelativePathSchema, U as OrV1Schema, V as listCategories, W as GraphEdgeSchema, X as DatasetAmbiguousError, Y as CatalogueSyntaxError, Z as DatasetNotFoundError, _ as EntrySchema, a as CatalogueIndexSchema, b as LicenceSchema, c as ManifestSchema, d as AliasSchema, et as RecipeError, f as ArchiveMemberSchema, g as CrosswalkReferenceSchema, h as ColumnNameSchema, i as listNamespaces, j as waybackUrl, k as parseEntry, l as SemVerSchema, m as CitationSchema, n as getManifest, o as createCatalogue, p as ArchiveSchema, q as JsonScalarSchema, r as listDatasets, s as LineageSchema, t as getDataset, tt as UnknownCategoryError, u as parseManifest, v as HttpUrlSchema, w as RecipeSchema, x as NamespaceSchema, y as IdSchema, z as getTarget } from "./bundled-NjZZ73vT.js";
2
2
  import { validateCrosswalk } from "twindex";
3
3
  //#region src/serialise.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { G as Entry, T as Dataset, at as Snapshot, i as CategoryTarget, st as Source } from "../categories-BVROO4tM.js";
1
+ import { G as Entry, T as Dataset, at as Snapshot, i as CategoryTarget, st as Source } from "../categories-frr8n_SI.js";
2
2
  //#region src/node/fetch-dataset.d.ts
3
3
  /** The network port a recipe downloads through: `downloadRaw` by default, which returns the bytes exactly as served, or a stand-in under test. */
4
4
  export type FetchPort = (url: string, init: {
@@ -1,4 +1,4 @@
1
- import { $ as NativeParseError, F as getOwn, J as ArchiveError, L as countTarget, Q as KeepsetError, et as RecipeError, t as getDataset, z as getTarget } from "../bundled-BmqpHjOH.js";
1
+ import { $ as NativeParseError, F as getOwn, J as ArchiveError, L as countTarget, Q as KeepsetError, et as RecipeError, t as getDataset, z as getTarget } from "../bundled-NjZZ73vT.js";
2
2
  import { RoundTripMismatchError, assertRoundTrip, encode } from "twindex";
3
3
  import { z } from "zod";
4
4
  import { createHash } from "node:crypto";
@@ -390,8 +390,8 @@ function columnNames(cells, idColumn, line) {
390
390
  return names;
391
391
  }
392
392
  /** The code unit of each character the scanner stops at. */
393
- const QUOTE = "\"".charCodeAt(0);
394
- const LINE_FEED = "\n".charCodeAt(0);
393
+ const QUOTE$1 = "\"".charCodeAt(0);
394
+ const LINE_FEED$1 = "\n".charCodeAt(0);
395
395
  const CARRIAGE_RETURN = "\r".charCodeAt(0);
396
396
  /** Scans a table file's text as it arrives in pieces, holding what a record in progress needs between them: the fields and field so far, whether a quoted field is open, and the line the record began on and the line the scan has reached. */
397
397
  var RecordScanner = class {
@@ -429,7 +429,7 @@ var RecordScanner = class {
429
429
  completed,
430
430
  carry: text.slice(index)
431
431
  };
432
- if (text.charCodeAt(index + 1) === QUOTE) {
432
+ if (text.charCodeAt(index + 1) === QUOTE$1) {
433
433
  this.field += "\"";
434
434
  index += 2;
435
435
  } else {
@@ -439,7 +439,7 @@ var RecordScanner = class {
439
439
  continue;
440
440
  }
441
441
  const code = text.charCodeAt(index);
442
- if (code === QUOTE) {
442
+ if (code === QUOTE$1) {
443
443
  if (this.field !== "") throw new NativeParseError("csv-table", this.line, "a quote opens inside an unquoted field");
444
444
  this.quoted = true;
445
445
  index += 1;
@@ -447,19 +447,19 @@ var RecordScanner = class {
447
447
  this.fields.push(this.field);
448
448
  this.field = "";
449
449
  index += 1;
450
- } else if (code === LINE_FEED || code === CARRIAGE_RETURN) {
450
+ } else if (code === LINE_FEED$1 || code === CARRIAGE_RETURN) {
451
451
  if (code === CARRIAGE_RETURN && index + 1 >= text.length && !final) return {
452
452
  completed,
453
453
  carry: text.slice(index)
454
454
  };
455
- index += code === CARRIAGE_RETURN && text.charCodeAt(index + 1) === LINE_FEED ? 2 : 1;
455
+ index += code === CARRIAGE_RETURN && text.charCodeAt(index + 1) === LINE_FEED$1 ? 2 : 1;
456
456
  this.line += 1;
457
457
  completed.push(this.endRecord());
458
458
  } else {
459
459
  let next = index + 1;
460
460
  while (next < text.length) {
461
461
  const following = text.charCodeAt(next);
462
- if (following === QUOTE || following === this.delimiterCode || following === LINE_FEED || following === CARRIAGE_RETURN) break;
462
+ if (following === QUOTE$1 || following === this.delimiterCode || following === LINE_FEED$1 || following === CARRIAGE_RETURN) break;
463
463
  next += 1;
464
464
  }
465
465
  this.field += text.slice(index, next);
@@ -1373,20 +1373,40 @@ const REDIRECTS = new Set(Object.values({
1373
1373
  }));
1374
1374
  /** What the downloader says it is, so a server operator can tell where the traffic comes from. */
1375
1375
  const USER_AGENT = "keepset (+https://github.com/Mearman/keepset)";
1376
+ /** The failure of a connection that sent nothing for the stall time. */
1377
+ var StalledError = class extends KeepsetError {};
1378
+ /**
1379
+ * Whether a failure is the connection's and may pass: a stall, or a socket error such as a reset, a broken pipe or a timeout, which carries an `E` code. A bad URL, a redirect that leads nowhere and an abort are not.
1380
+ * @param error - The failure.
1381
+ * @returns True when asking again may succeed.
1382
+ */
1383
+ function isConnectionFailure(error) {
1384
+ return error instanceof StalledError || typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && /^E[A-Z]+$/.test(error.code) && error.code !== "ERR_INVALID_URL";
1385
+ }
1376
1386
  /**
1377
1387
  * Sends one GET and resolves with the response head, its body still unread.
1378
1388
  * @param url - The URL.
1379
1389
  * @param signal - Aborts the request.
1390
+ * @param stall - How many milliseconds the connection may send nothing, in the head or the body that follows.
1380
1391
  * @returns The response.
1392
+ * @throws KeepsetError when the connection stalls.
1381
1393
  */
1382
- async function get(url, signal) {
1394
+ async function get(url, signal, stall) {
1383
1395
  const send = url.protocol === "https:" ? request$1 : request;
1384
1396
  return new Promise((resolve, reject) => {
1397
+ let answered;
1385
1398
  const request = send(url, {
1386
1399
  method: "GET",
1387
1400
  headers: { "user-agent": USER_AGENT },
1388
1401
  ...signal === void 0 ? {} : { signal }
1389
- }, resolve);
1402
+ }, (response) => {
1403
+ answered = response;
1404
+ resolve(response);
1405
+ });
1406
+ request.setTimeout(stall, () => {
1407
+ const dead = new StalledError(`${url.href} sent nothing for ${String(stall)} milliseconds, so the connection was given up as dead`);
1408
+ (answered ?? request).destroy(dead);
1409
+ });
1390
1410
  request.on("error", reject);
1391
1411
  request.end();
1392
1412
  });
@@ -1407,13 +1427,13 @@ async function readBody(response) {
1407
1427
  /**
1408
1428
  * Requests one URL, following its redirects by recursion.
1409
1429
  * @param url - The URL to request.
1410
- * @param signal - Aborts the request.
1430
+ * @param init - The request options.
1411
1431
  * @param redirects - How many redirects led here.
1412
1432
  * @returns The final response.
1413
1433
  */
1414
- async function follow(url, signal, redirects) {
1434
+ async function follow(url, init, redirects) {
1415
1435
  if (url.protocol !== "http:" && url.protocol !== "https:") throw new KeepsetError(`${url.href} is not an http or https URL`);
1416
- const response = await get(url, signal);
1436
+ const response = await get(url, init.signal, init.stallMilliseconds ?? 12e4);
1417
1437
  const { statusCode: status, statusMessage } = response;
1418
1438
  if (status === void 0 || statusMessage === void 0) throw new KeepsetError(`${url.href} answered without a status line`);
1419
1439
  if (!REDIRECTS.has(status)) return new Response(await readBody(response), {
@@ -1424,17 +1444,36 @@ async function follow(url, signal, redirects) {
1424
1444
  const { location } = response.headers;
1425
1445
  if (location === void 0) throw new KeepsetError(`${url.href} answered ${String(status)} without a location`);
1426
1446
  if (redirects === MAX_REDIRECTS) throw new KeepsetError(`${url.href} redirected more than ${String(MAX_REDIRECTS)} times`);
1427
- return follow(new URL(location, url), signal, redirects + 1);
1447
+ return follow(new URL(location, url), init, redirects + 1);
1428
1448
  }
1429
1449
  /**
1430
1450
  * Downloads a URL's bytes exactly as the server sends them, following redirects. Unlike the platform `fetch`, it never undoes a `Content-Encoding`: servers commonly label a `.gz` file `Content-Encoding: gzip`, and a recipe that unpacks gzip needs the file as published, which is also what its SHA-256 is recorded over. No `Accept-Encoding` is sent, so a server that compresses on the fly has no reason to.
1431
1451
  * @param url - An http or https URL.
1432
1452
  * @param init - The request options.
1433
1453
  * @returns A Response holding the raw bytes, with the final status.
1434
- * @throws KeepsetError when a redirect has no location, leaves http and https, or passes the redirect limit.
1454
+ * @throws KeepsetError when a redirect has no location, leaves http and https, or passes the redirect limit; the connection's failure, after the tries, when it keeps failing or sends nothing for longer than the stall time.
1435
1455
  */
1436
1456
  async function downloadRaw(url, init) {
1437
- return follow(new URL(url), init.signal, 0);
1457
+ return tryDownload(url, init, 1);
1458
+ }
1459
+ /**
1460
+ * One try of a download, and the tries after it while the connection fails.
1461
+ * @param url - The URL.
1462
+ * @param init - The request options.
1463
+ * @param attempt - Which try this is, from 1.
1464
+ * @returns The response.
1465
+ * @throws The last failure when the tries are used up, or at once for a failure that is not the connection's.
1466
+ */
1467
+ async function tryDownload(url, init, attempt) {
1468
+ try {
1469
+ return await follow(new URL(url), init, 0);
1470
+ } catch (error) {
1471
+ if (attempt >= 3 || init.signal?.aborted === true || !isConnectionFailure(error)) throw error;
1472
+ await new Promise((resolve) => {
1473
+ setTimeout(resolve, (init.retryDelayMilliseconds ?? 3e3) * attempt);
1474
+ });
1475
+ return tryDownload(url, init, attempt + 1);
1476
+ }
1438
1477
  }
1439
1478
  //#endregion
1440
1479
  //#region src/node/paged.ts
@@ -1481,7 +1520,7 @@ const FeatureListSchema = z.array(z.unknown());
1481
1520
  async function readPages(layer, held) {
1482
1521
  if (held.length >= layer.count) return held;
1483
1522
  const url = `${layer.query}?where=1%3D1&outFields=*&orderByFields=${encodeURIComponent(layer.orderBy)}&resultOffset=${String(held.length)}&f=geojson`;
1484
- const page = await getJson(layer.port, url, layer.signal);
1523
+ const page = await withAttempts(async () => getJson(layer.port, url, layer.signal), layer.signal);
1485
1524
  const features = FeatureListSchema.safeParse(typeof page === "object" && page !== null && "features" in page ? page.features : void 0);
1486
1525
  if (!features.success || features.data.length === 0) throw new KeepsetError(`${layer.query} gave no features at offset ${String(held.length)}, before the ${String(layer.count)} it reports, so it does not page by offset`);
1487
1526
  return readPages(layer, [...held, ...features.data]);
@@ -1511,6 +1550,95 @@ async function downloadArcgisLayer(layerUrl, orderBy, port, signal) {
1511
1550
  features: pages
1512
1551
  }));
1513
1552
  }
1553
+ /** How many times a page is requested before the download fails: a connection can drop or go silent in one request of dozens and succeed in the next, and the pages are read by offset so asking again is exact. */
1554
+ const PAGE_ATTEMPTS = 3;
1555
+ /**
1556
+ * Runs one page read, asking again when it fails, so one bad response of a long download does not lose the rest.
1557
+ * @param read - The page read.
1558
+ * @param signal - Aborts the download, which is not asked again.
1559
+ * @param attempt - Which attempt this is, from 1.
1560
+ * @returns What the read returns.
1561
+ * @throws The last failure, when the attempts are used up or the download was aborted.
1562
+ */
1563
+ async function withAttempts(read, signal, attempt = 1) {
1564
+ try {
1565
+ return await read();
1566
+ } catch (error) {
1567
+ if (attempt >= PAGE_ATTEMPTS || signal?.aborted === true) throw error;
1568
+ return withAttempts(read, signal, attempt + 1);
1569
+ }
1570
+ }
1571
+ const LINE_FEED = 10;
1572
+ const QUOTE = 34;
1573
+ /**
1574
+ * How many CSV records a response holds: the line feeds outside a quoted field, and one more when the last record has no line feed.
1575
+ * @param bytes - A CSV response.
1576
+ * @returns The number of records, the header included.
1577
+ */
1578
+ function recordsIn(bytes) {
1579
+ let records = 0;
1580
+ let quoted = false;
1581
+ for (const byte of bytes) if (byte === QUOTE) quoted = !quoted;
1582
+ else if (byte === LINE_FEED && !quoted) records += 1;
1583
+ return bytes.length > 0 && bytes[bytes.length - 1] !== LINE_FEED ? records + 1 : records;
1584
+ }
1585
+ /**
1586
+ * Reads one response of a Socrata resource endpoint as bytes.
1587
+ * @param port - The network port.
1588
+ * @param url - The request URL.
1589
+ * @param signal - Aborts the request.
1590
+ * @returns The response body.
1591
+ * @throws KeepsetError when the server does not answer successfully.
1592
+ */
1593
+ async function getBytes(port, url, signal) {
1594
+ const response = await port(url, signal === void 0 ? {} : { signal });
1595
+ if (!response.ok) throw new KeepsetError(`${url} answered ${String(response.status)} ${response.statusText}`);
1596
+ return new Uint8Array(await response.arrayBuffer());
1597
+ }
1598
+ /**
1599
+ * Reads a resource's pages, each starting where the rows held so far end, and checks each holds exactly the rows it should, so a response cut short is an error and not a table with rows missing.
1600
+ * @param read - The resource read.
1601
+ * @param held - The pages read so far, each after the first without its header record.
1602
+ * @param offset - How many rows the pages hold.
1603
+ * @returns Every page.
1604
+ * @throws KeepsetError when a page holds a different number of rows than the count says it should, or its header differs from the first page's.
1605
+ */
1606
+ async function readResourcePages(read, held, offset) {
1607
+ if (offset >= read.count) return held;
1608
+ const expected = Math.min(read.pageSize, read.count - offset);
1609
+ const page = await withAttempts(async () => {
1610
+ const bytes = await getBytes(read.port, `${read.resource}?%24order=%3Aid&%24limit=${String(read.pageSize)}&%24offset=${String(offset)}`, read.signal);
1611
+ const rows = recordsIn(bytes) - 1;
1612
+ if (rows !== expected) throw new KeepsetError(`${read.resource} gave ${String(rows)} rows at offset ${String(offset)} where ${String(expected)} of its ${String(read.count)} were due, so a response was cut short or the table changed during the download`);
1613
+ return bytes;
1614
+ }, read.signal);
1615
+ const first = held[0];
1616
+ const headerEnd = page.indexOf(LINE_FEED) + 1;
1617
+ if (first !== void 0 && !page.subarray(0, headerEnd).every((byte, index) => byte === first[index])) throw new KeepsetError(`${read.resource} gave a different header at offset ${String(offset)} than at the start`);
1618
+ return readResourcePages(read, [...held, first === void 0 ? page : page.subarray(headerEnd)], offset + expected);
1619
+ }
1620
+ /**
1621
+ * Downloads a whole Socrata resource as one CSV: the row count is read first, then pages of `pageSize` rows in the order of the rows' ids, each checked to hold the rows it should, and joined under the first page's header.
1622
+ * @param resource - The resource's URL, such as `https://data.example.org/resource/abcd-1234.csv`, without a query.
1623
+ * @param pageSize - How many rows a page holds.
1624
+ * @param port - The network port.
1625
+ * @param signal - Aborts the download.
1626
+ * @returns The bytes of one CSV, which is what the parser reads and what the provenance hashes.
1627
+ * @throws KeepsetError when the resource does not report a count, or a page is not what it should be.
1628
+ */
1629
+ async function downloadSocrataResource(resource, pageSize, port, signal) {
1630
+ const reported = /^"count"\r?\n"(\d+)"/.exec(new TextDecoder("utf-8").decode(await getBytes(port, `${resource}?%24select=count(*)`, signal)));
1631
+ if (reported?.[1] === void 0) throw new KeepsetError(`${resource} did not report a row count`);
1632
+ const pages = await readResourcePages({
1633
+ resource,
1634
+ pageSize,
1635
+ count: Number(reported[1]),
1636
+ port,
1637
+ signal
1638
+ }, [], 0);
1639
+ if (pages.length === 0) throw new KeepsetError(`${resource} has no rows`);
1640
+ return Buffer.concat(pages);
1641
+ }
1514
1642
  /**
1515
1643
  * Downloads a paged source as the one file its pages make, in the way its recipe's paging says.
1516
1644
  * @param url - The source's URL.
@@ -1521,7 +1649,7 @@ async function downloadArcgisLayer(layerUrl, orderBy, port, signal) {
1521
1649
  * @throws KeepsetError when a page cannot be read or the pages do not add up to what the source reports.
1522
1650
  */
1523
1651
  async function downloadPaged(url, paging, port, signal) {
1524
- return downloadArcgisLayer(url, paging.orderBy, port, signal);
1652
+ return paging.kind === "arcgis-query" ? downloadArcgisLayer(url, paging.orderBy, port, signal) : downloadSocrataResource(url, paging.pageSize, port, signal);
1525
1653
  }
1526
1654
  //#endregion
1527
1655
  //#region src/node/fetch-dataset.ts
@@ -4,7 +4,7 @@ import { KeepsetError, NAME_SEPARATOR, countTarget, getDataset, listDatasets, li
4
4
  import { fetchDataset, roundTripDataset } from "keepset/node";
5
5
  import { parseArgs } from "node:util";
6
6
  //#region package.json
7
- var version = "0.20.0";
7
+ var version = "0.21.0";
8
8
  const usage = `Usage: keepset <command> [options]
9
9
 
10
10
  Commands:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "keepset",
3
3
  "description": "Library, CLI and statically generated catalogue for dataset fetch-and-normalise recipes",
4
- "version": "0.20.0",
4
+ "version": "0.21.0",
5
5
  "bin": {
6
6
  "keepset": "./dist-cli/keepset.mjs"
7
7
  },
@@ -1 +1 @@
1
- {"$defs":{"Crosswalk":{"$ref":"https://cdn.jsdelivr.net/npm/twindex@0.1.4/schemas/twindex.schema.json"},"Entry":{"additionalProperties":false,"properties":{"aliases":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*(?:\\/[A-Za-z0-9][A-Za-z0-9._-]*)*$","type":"string"},"minItems":1,"type":"array"},"category":{"enum":["graph/v1","or/v1"],"type":"string"},"citations":{"items":{"additionalProperties":false,"properties":{"doi":{"pattern":"^10\\.\\d{4,9}\\/\\S+$","type":"string"},"text":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["text"],"type":"object"},"type":"array"},"crosswalk":{"anyOf":[{"$ref":"#/$defs/Crosswalk"},{"additionalProperties":false,"properties":{"file":{"type":"string"}},"required":["file"],"type":"object"}]},"description":{"minLength":1,"type":"string"},"domain":{"minLength":1,"type":"string"},"expectedCounts":{"additionalProperties":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"licence":{"oneOf":[{"additionalProperties":false,"properties":{"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*$","type":"string"},"kind":{"const":"spdx","type":"string"}},"required":["kind","id"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"custom","type":"string"},"name":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["kind","name","url"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"unrecorded","type":"string"}},"required":["kind"],"type":"object"}]},"name":{"minLength":1,"type":"string"},"namespace":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"properties":{"additionalProperties":{"type":["string","number","boolean","null"]},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"recipe":{"oneOf":[{"additionalProperties":false,"properties":{"archive":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"gzip","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"zip","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar.gz","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"xlsx","type":"string"},"sheet":{"minLength":1,"type":"string"}},"required":["kind","sheet"],"type":"object"}]},"paging":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"arcgis-query","type":"string"},"orderBy":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$","type":"string"}},"required":["kind","orderBy"],"type":"object"}]},"parser":{"oneOf":[{"additionalProperties":false,"properties":{"commentPrefixes":{"items":{"minLength":1,"type":"string"},"type":"array"},"delimiter":{"enum":["detect","whitespace","tab","comma","space"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"edge-list","type":"string"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"sourceColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"targetColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","commentPrefixes","sourceColumn","targetColumn"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"matrix-market","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"gml","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"pajek","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"columns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"type":"array"},"delimiter":{"enum":["comma","tab","semicolon"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"csv-table","type":"string"},"idColumn":{"anyOf":[{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},{"type":"null"}]},"numericColumns":{"items":{"additionalProperties":false,"properties":{"column":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"factor":{"exclusiveMinimum":0,"type":"number"}},"required":["column","factor"],"type":"object"},"type":"array"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","idColumn","numericColumns"],"type":"object"},{"additionalProperties":false,"properties":{"columns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"minItems":1,"type":"array"},"dateColumns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"type":"array"},"encoding":{"const":"utf-8","type":"string"},"format":{"const":"geojson","type":"string"},"idColumn":{"anyOf":[{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},{"type":"null"}]},"numericColumns":{"items":{"additionalProperties":false,"properties":{"column":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"factor":{"exclusiveMinimum":0,"type":"number"}},"required":["column","factor"],"type":"object"},"type":"array"}},"required":["format","encoding","idColumn","numericColumns","dateColumns","columns"],"type":"object"}]},"source":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"status":{"const":"fetchable","type":"string"}},"required":["status","source","archive","parser"],"type":"object"},{"additionalProperties":false,"properties":{"reason":{"minLength":1,"type":"string"},"status":{"const":"not-fetchable","type":"string"}},"required":["status","reason"],"type":"object"}]},"sources":{"items":{"additionalProperties":false,"properties":{"retrieved":{"format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$","type":"string"},"snapshot":{"additionalProperties":false,"properties":{"timestamp":{"pattern":"^\\d{8}(?:\\d{6})?$","type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","timestamp"],"type":"object"},"status":{"enum":["live","dead"],"type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","retrieved","status"],"type":"object"},"minItems":1,"type":"array"},"tags":{"items":{"minLength":1,"type":"string"},"type":"array"}},"required":["id","name","tags","category","sources","licence","citations","recipe","crosswalk","expectedCounts","properties"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.20.0/schemas/entry.schema.json","$ref":"#/$defs/Entry","$schema":"https://json-schema.org/draft/2020-12/schema"}
1
+ {"$defs":{"Crosswalk":{"$ref":"https://cdn.jsdelivr.net/npm/twindex@0.1.4/schemas/twindex.schema.json"},"Entry":{"additionalProperties":false,"properties":{"aliases":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*(?:\\/[A-Za-z0-9][A-Za-z0-9._-]*)*$","type":"string"},"minItems":1,"type":"array"},"category":{"enum":["graph/v1","or/v1"],"type":"string"},"citations":{"items":{"additionalProperties":false,"properties":{"doi":{"pattern":"^10\\.\\d{4,9}\\/\\S+$","type":"string"},"text":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["text"],"type":"object"},"type":"array"},"crosswalk":{"anyOf":[{"$ref":"#/$defs/Crosswalk"},{"additionalProperties":false,"properties":{"file":{"type":"string"}},"required":["file"],"type":"object"}]},"description":{"minLength":1,"type":"string"},"domain":{"minLength":1,"type":"string"},"expectedCounts":{"additionalProperties":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"licence":{"oneOf":[{"additionalProperties":false,"properties":{"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9.+-]*$","type":"string"},"kind":{"const":"spdx","type":"string"}},"required":["kind","id"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"custom","type":"string"},"name":{"minLength":1,"type":"string"},"url":{"format":"uri","type":"string"}},"required":["kind","name","url"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"unrecorded","type":"string"}},"required":["kind"],"type":"object"}]},"name":{"minLength":1,"type":"string"},"namespace":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"properties":{"additionalProperties":{"type":["string","number","boolean","null"]},"propertyNames":{"pattern":"^[A-Za-z][A-Za-z0-9]*$","type":"string"},"type":"object"},"recipe":{"oneOf":[{"additionalProperties":false,"properties":{"archive":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"none","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"gzip","type":"string"}},"required":["kind"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"zip","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"tar.gz","type":"string"},"member":{"anyOf":[{"additionalProperties":false,"properties":{"path":{"type":"string"}},"required":["path"],"type":"object"},{"additionalProperties":false,"properties":{"extensions":{"items":{"pattern":"^\\.[A-Za-z0-9][A-Za-z0-9.]*$","type":"string"},"minItems":1,"type":"array"}},"required":["extensions"],"type":"object"}]}},"required":["kind","member"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"xlsx","type":"string"},"sheet":{"minLength":1,"type":"string"}},"required":["kind","sheet"],"type":"object"}]},"paging":{"oneOf":[{"additionalProperties":false,"properties":{"kind":{"const":"arcgis-query","type":"string"},"orderBy":{"pattern":"^[A-Za-z_][A-Za-z0-9_]*$","type":"string"}},"required":["kind","orderBy"],"type":"object"},{"additionalProperties":false,"properties":{"kind":{"const":"socrata-resource","type":"string"},"pageSize":{"maximum":9007199254740991,"minimum":1,"type":"integer"}},"required":["kind","pageSize"],"type":"object"}]},"parser":{"oneOf":[{"additionalProperties":false,"properties":{"commentPrefixes":{"items":{"minLength":1,"type":"string"},"type":"array"},"delimiter":{"enum":["detect","whitespace","tab","comma","space"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"edge-list","type":"string"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"sourceColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"targetColumn":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","commentPrefixes","sourceColumn","targetColumn"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"matrix-market","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"gml","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"pajek","type":"string"}},"required":["format","encoding"],"type":"object"},{"additionalProperties":false,"properties":{"columns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"type":"array"},"delimiter":{"enum":["comma","tab","semicolon"],"type":"string"},"encoding":{"enum":["utf-8","latin1"],"type":"string"},"format":{"const":"csv-table","type":"string"},"idColumn":{"anyOf":[{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},{"type":"null"}]},"numericColumns":{"items":{"additionalProperties":false,"properties":{"column":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"factor":{"exclusiveMinimum":0,"type":"number"}},"required":["column","factor"],"type":"object"},"type":"array"},"skipLines":{"maximum":9007199254740991,"minimum":0,"type":"integer"}},"required":["format","encoding","delimiter","skipLines","idColumn","numericColumns"],"type":"object"},{"additionalProperties":false,"properties":{"columns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"minItems":1,"type":"array"},"dateColumns":{"items":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"type":"array"},"encoding":{"const":"utf-8","type":"string"},"format":{"const":"geojson","type":"string"},"idColumn":{"anyOf":[{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},{"type":"null"}]},"numericColumns":{"items":{"additionalProperties":false,"properties":{"column":{"pattern":"^[A-Za-z](?:[A-Za-z0-9_]*[A-Za-z0-9])?$","type":"string"},"factor":{"exclusiveMinimum":0,"type":"number"}},"required":["column","factor"],"type":"object"},"type":"array"}},"required":["format","encoding","idColumn","numericColumns","dateColumns","columns"],"type":"object"}]},"source":{"maximum":9007199254740991,"minimum":0,"type":"integer"},"status":{"const":"fetchable","type":"string"}},"required":["status","source","archive","parser"],"type":"object"},{"additionalProperties":false,"properties":{"reason":{"minLength":1,"type":"string"},"status":{"const":"not-fetchable","type":"string"}},"required":["status","reason"],"type":"object"}]},"sources":{"items":{"additionalProperties":false,"properties":{"retrieved":{"format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$","type":"string"},"snapshot":{"additionalProperties":false,"properties":{"timestamp":{"pattern":"^\\d{8}(?:\\d{6})?$","type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","timestamp"],"type":"object"},"status":{"enum":["live","dead"],"type":"string"},"url":{"format":"uri","type":"string"}},"required":["url","retrieved","status"],"type":"object"},"minItems":1,"type":"array"},"tags":{"items":{"minLength":1,"type":"string"},"type":"array"}},"required":["id","name","tags","category","sources","licence","citations","recipe","crosswalk","expectedCounts","properties"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.21.0/schemas/entry.schema.json","$ref":"#/$defs/Entry","$schema":"https://json-schema.org/draft/2020-12/schema"}
@@ -1 +1 @@
1
- {"$defs":{"Manifest":{"additionalProperties":false,"properties":{"description":{"minLength":1,"type":"string"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"lineage":{"additionalProperties":false,"properties":{"catalogue":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["catalogue","version","repository"],"type":"object"},"name":{"minLength":1,"type":"string"},"namespaceAliases":{"additionalProperties":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"propertyNames":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"type":"object"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["id","name","description","version","repository"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.20.0/schemas/manifest.schema.json","$ref":"#/$defs/Manifest","$schema":"https://json-schema.org/draft/2020-12/schema"}
1
+ {"$defs":{"Manifest":{"additionalProperties":false,"properties":{"description":{"minLength":1,"type":"string"},"id":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"lineage":{"additionalProperties":false,"properties":{"catalogue":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["catalogue","version","repository"],"type":"object"},"name":{"minLength":1,"type":"string"},"namespaceAliases":{"additionalProperties":{"items":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"minItems":1,"type":"array"},"propertyNames":{"pattern":"^[A-Za-z0-9][A-Za-z0-9._-]*$","type":"string"},"type":"object"},"repository":{"format":"uri","type":"string"},"version":{"pattern":"^(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$","type":"string"}},"required":["id","name","description","version","repository"],"type":"object"}},"$id":"https://cdn.jsdelivr.net/npm/keepset@0.21.0/schemas/manifest.schema.json","$ref":"#/$defs/Manifest","$schema":"https://json-schema.org/draft/2020-12/schema"}