georeference 0.0.0-stage → 1.0.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.
- package/CHANGELOG.md +19 -0
- package/CONTRIBUTING.fr.md +44 -0
- package/CONTRIBUTING.md +44 -0
- package/LICENSE +21 -0
- package/README.fr.md +84 -0
- package/README.md +83 -2
- package/SECURITY.fr.md +9 -0
- package/SECURITY.md +9 -0
- package/dist/adapters/common.d.ts +5 -0
- package/dist/adapters/common.js +37 -0
- package/dist/adapters/common.js.map +1 -0
- package/dist/adapters/csv.d.ts +2 -0
- package/dist/adapters/csv.js +32 -0
- package/dist/adapters/csv.js.map +1 -0
- package/dist/adapters/geojson.d.ts +2 -0
- package/dist/adapters/geojson.js +36 -0
- package/dist/adapters/geojson.js.map +1 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +10 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/json.d.ts +3 -0
- package/dist/adapters/json.js +48 -0
- package/dist/adapters/json.js.map +1 -0
- package/dist/adapters/postgres.d.ts +3 -0
- package/dist/adapters/postgres.js +59 -0
- package/dist/adapters/postgres.js.map +1 -0
- package/dist/adapters/sqlite.d.ts +2 -0
- package/dist/adapters/sqlite.js +49 -0
- package/dist/adapters/sqlite.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +219 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +24 -0
- package/dist/client.js.map +1 -0
- package/dist/export.d.ts +6 -0
- package/dist/export.js +127 -0
- package/dist/export.js.map +1 -0
- package/dist/files.d.ts +6 -0
- package/dist/files.js +28 -0
- package/dist/files.js.map +1 -0
- package/dist/generate.d.ts +10 -0
- package/dist/generate.js +102 -0
- package/dist/generate.js.map +1 -0
- package/dist/graph.d.ts +3 -0
- package/dist/graph.js +31 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/package.d.ts +6 -0
- package/dist/package.js +133 -0
- package/dist/package.js.map +1 -0
- package/dist/seeding/index.d.ts +6 -0
- package/dist/seeding/index.js +80 -0
- package/dist/seeding/index.js.map +1 -0
- package/dist/seeding/ledger.d.ts +12 -0
- package/dist/seeding/ledger.js +54 -0
- package/dist/seeding/ledger.js.map +1 -0
- package/dist/seeding/mapping.d.ts +31 -0
- package/dist/seeding/mapping.js +54 -0
- package/dist/seeding/mapping.js.map +1 -0
- package/dist/seeding/preflight.d.ts +10 -0
- package/dist/seeding/preflight.js +149 -0
- package/dist/seeding/preflight.js.map +1 -0
- package/dist/seeding/retained.d.ts +5 -0
- package/dist/seeding/retained.js +32 -0
- package/dist/seeding/retained.js.map +1 -0
- package/dist/seeding/types.d.ts +41 -0
- package/dist/seeding/types.js +2 -0
- package/dist/seeding/types.js.map +1 -0
- package/dist/seeding/write.d.ts +4 -0
- package/dist/seeding/write.js +31 -0
- package/dist/seeding/write.js.map +1 -0
- package/dist/snapshot.d.ts +14 -0
- package/dist/snapshot.js +154 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/sources/geonames.d.ts +14 -0
- package/dist/sources/geonames.js +118 -0
- package/dist/sources/geonames.js.map +1 -0
- package/dist/types.d.ts +113 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/validation.d.ts +4 -0
- package/dist/validation.js +91 -0
- package/dist/validation.js.map +1 -0
- package/docs/architecture.fr.md +105 -0
- package/docs/architecture.md +105 -0
- package/docs/data-sources.fr.md +39 -0
- package/docs/data-sources.md +39 -0
- package/docs/format-decisions.fr.md +54 -0
- package/docs/format-decisions.md +54 -0
- package/docs/formats.fr.md +34 -0
- package/docs/formats.md +34 -0
- package/docs/generation.fr.md +29 -0
- package/docs/generation.md +29 -0
- package/docs/getting-started.fr.md +79 -0
- package/docs/getting-started.md +79 -0
- package/docs/seeding.fr.md +71 -0
- package/docs/seeding.md +71 -0
- package/examples/README.md +7 -0
- package/examples/metadata.json +13 -0
- package/examples/sample.ndjson +4 -0
- package/package.json +64 -4
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { project, validateOptions, writeChunks } from "./common.js";
|
|
2
|
+
export const POSTGRES_SCHEMA = `CREATE TABLE IF NOT EXISTS "georeference_entities" (
|
|
3
|
+
"id" TEXT PRIMARY KEY,
|
|
4
|
+
"entity_type" TEXT NOT NULL,
|
|
5
|
+
"name" TEXT NOT NULL,
|
|
6
|
+
"country_id" TEXT NOT NULL,
|
|
7
|
+
"parent_id" TEXT REFERENCES "georeference_entities"("id") DEFERRABLE INITIALLY DEFERRED,
|
|
8
|
+
"codes" JSONB NOT NULL DEFAULT '{}',
|
|
9
|
+
"names" JSONB NOT NULL DEFAULT '{}',
|
|
10
|
+
"level" INTEGER,
|
|
11
|
+
"coordinates" JSONB,
|
|
12
|
+
"boundary" JSONB
|
|
13
|
+
);
|
|
14
|
+
CREATE INDEX IF NOT EXISTS "georeference_entities_parent_idx" ON "georeference_entities" ("parent_id");
|
|
15
|
+
`;
|
|
16
|
+
function literal(value, json = false) {
|
|
17
|
+
if (value === null)
|
|
18
|
+
return 'NULL';
|
|
19
|
+
if (typeof value === 'number')
|
|
20
|
+
return String(value);
|
|
21
|
+
if (value.includes('\0'))
|
|
22
|
+
throw new Error('PostgreSQL text cannot contain NUL characters');
|
|
23
|
+
return `'${value.replaceAll("'", "''")}'${json ? '::jsonb' : ''}`;
|
|
24
|
+
}
|
|
25
|
+
function values(entity) {
|
|
26
|
+
const result = [
|
|
27
|
+
['id', literal(entity.id)], ['entity_type', literal(entity.entityType)], ['name', literal(entity.name)],
|
|
28
|
+
['country_id', literal(entity.countryId)], ['parent_id', literal(entity.parentId)],
|
|
29
|
+
];
|
|
30
|
+
for (const key of ['codes', 'names', 'coordinates', 'boundary']) {
|
|
31
|
+
if (Object.hasOwn(entity, key))
|
|
32
|
+
result.push([key, entity[key] === null ? 'NULL' : literal(JSON.stringify(entity[key]), true)]);
|
|
33
|
+
}
|
|
34
|
+
if (Object.hasOwn(entity, 'level'))
|
|
35
|
+
result.push(['level', literal(entity.level ?? null)]);
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
export const postgresAdapter = {
|
|
39
|
+
format: 'sql-postgres', version: '1.0.0', validate: validateOptions,
|
|
40
|
+
async write(context) {
|
|
41
|
+
await writeChunks(context, 'entities.sql', (function* () {
|
|
42
|
+
yield '-- GeoReference PostgreSQL export. data-only requires a compatible existing schema.\n';
|
|
43
|
+
yield 'BEGIN;\nSET LOCAL standard_conforming_strings = on;\n';
|
|
44
|
+
if (context.plan.request.schemaMode !== 'data-only')
|
|
45
|
+
yield POSTGRES_SCHEMA;
|
|
46
|
+
for (const original of context.plan.entities()) {
|
|
47
|
+
const row = values(project(original, context.plan.request));
|
|
48
|
+
const columns = row.map(([key]) => `"${key}"`).join(', ');
|
|
49
|
+
const mutable = row.filter(([key]) => key !== 'id');
|
|
50
|
+
const conflict = context.plan.request.sqlMode === 'insert-only' ? 'DO NOTHING'
|
|
51
|
+
: `DO UPDATE SET ${mutable.map(([key]) => `"${key}" = EXCLUDED."${key}"`).join(', ')} WHERE ${mutable.map(([key]) => `"georeference_entities"."${key}" IS DISTINCT FROM EXCLUDED."${key}"`).join(' OR ')}`;
|
|
52
|
+
yield `INSERT INTO "georeference_entities" (${columns}) VALUES (${row.map(([, value]) => value).join(', ')}) ON CONFLICT ("id") ${conflict};\n`;
|
|
53
|
+
}
|
|
54
|
+
yield 'COMMIT;\n';
|
|
55
|
+
})());
|
|
56
|
+
return ['entities.sql'];
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/adapters/postgres.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Entity, ExportAdapter } from '../types.ts';\nimport { project, validateOptions, writeChunks } from './common.ts';\n\nexport const POSTGRES_SCHEMA = `CREATE TABLE IF NOT EXISTS \"georeference_entities\" (\n \"id\" TEXT PRIMARY KEY,\n \"entity_type\" TEXT NOT NULL,\n \"name\" TEXT NOT NULL,\n \"country_id\" TEXT NOT NULL,\n \"parent_id\" TEXT REFERENCES \"georeference_entities\"(\"id\") DEFERRABLE INITIALLY DEFERRED,\n \"codes\" JSONB NOT NULL DEFAULT '{}',\n \"names\" JSONB NOT NULL DEFAULT '{}',\n \"level\" INTEGER,\n \"coordinates\" JSONB,\n \"boundary\" JSONB\n);\nCREATE INDEX IF NOT EXISTS \"georeference_entities_parent_idx\" ON \"georeference_entities\" (\"parent_id\");\n`;\n\nfunction literal(value: string | number | null, json = false): string {\n if (value === null) return 'NULL';\n if (typeof value === 'number') return String(value);\n if (value.includes('\\0')) throw new Error('PostgreSQL text cannot contain NUL characters');\n return `'${value.replaceAll(\"'\", \"''\")}'${json ? '::jsonb' : ''}`;\n}\n\nfunction values(entity: Entity): [string, string][] {\n const result: [string, string][] = [\n ['id', literal(entity.id)], ['entity_type', literal(entity.entityType)], ['name', literal(entity.name)],\n ['country_id', literal(entity.countryId)], ['parent_id', literal(entity.parentId)],\n ];\n for (const key of ['codes', 'names', 'coordinates', 'boundary'] as const) {\n if (Object.hasOwn(entity, key)) result.push([key, entity[key] === null ? 'NULL' : literal(JSON.stringify(entity[key]), true)]);\n }\n if (Object.hasOwn(entity, 'level')) result.push(['level', literal(entity.level ?? null)]);\n return result;\n}\n\nexport const postgresAdapter: ExportAdapter = {\n format: 'sql-postgres', version: '1.0.0', validate: validateOptions,\n async write(context) {\n await writeChunks(context, 'entities.sql', (function* () {\n yield '-- GeoReference PostgreSQL export. data-only requires a compatible existing schema.\\n';\n yield 'BEGIN;\\nSET LOCAL standard_conforming_strings = on;\\n';\n if (context.plan.request.schemaMode !== 'data-only') yield POSTGRES_SCHEMA;\n for (const original of context.plan.entities()) {\n const row = values(project(original, context.plan.request));\n const columns = row.map(([key]) => `\"${key}\"`).join(', ');\n const mutable = row.filter(([key]) => key !== 'id');\n const conflict = context.plan.request.sqlMode === 'insert-only' ? 'DO NOTHING'\n : `DO UPDATE SET ${mutable.map(([key]) => `\"${key}\" = EXCLUDED.\"${key}\"`).join(', ')} WHERE ${mutable.map(([key]) => `\"georeference_entities\".\"${key}\" IS DISTINCT FROM EXCLUDED.\"${key}\"`).join(' OR ')}`;\n yield `INSERT INTO \"georeference_entities\" (${columns}) VALUES (${row.map(([, value]) => value).join(', ')}) ON CONFLICT (\"id\") ${conflict};\\n`;\n }\n yield 'COMMIT;\\n';\n })());\n return ['entities.sql'];\n },\n};\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
3
|
+
import { project, validateOptions } from "./common.js";
|
|
4
|
+
export const sqliteAdapter = {
|
|
5
|
+
format: 'sqlite', version: '1.0.0', validate: validateOptions,
|
|
6
|
+
async write(context) {
|
|
7
|
+
if (context.plan.externalReferences.length)
|
|
8
|
+
throw new Error('Standalone SQLite requires included ancestors; unresolved references cannot be delivered as a ready-to-use database');
|
|
9
|
+
context.signal?.throwIfAborted();
|
|
10
|
+
const database = new DatabaseSync(join(context.directory, 'entities.sqlite'));
|
|
11
|
+
try {
|
|
12
|
+
database.exec(`PRAGMA foreign_keys = ON;
|
|
13
|
+
CREATE TABLE georeference_metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
14
|
+
CREATE TABLE georeference_entities (
|
|
15
|
+
id TEXT PRIMARY KEY, entity_type TEXT NOT NULL, name TEXT NOT NULL, country_id TEXT NOT NULL,
|
|
16
|
+
parent_id TEXT REFERENCES georeference_entities(id) DEFERRABLE INITIALLY DEFERRED,
|
|
17
|
+
codes TEXT NOT NULL DEFAULT '{}', names TEXT NOT NULL DEFAULT '{}', level INTEGER,
|
|
18
|
+
coordinates TEXT, boundary TEXT
|
|
19
|
+
);
|
|
20
|
+
CREATE INDEX georeference_entities_parent_idx ON georeference_entities(parent_id);
|
|
21
|
+
CREATE INDEX georeference_entities_country_idx ON georeference_entities(country_id);
|
|
22
|
+
BEGIN;`);
|
|
23
|
+
database.prepare('INSERT INTO georeference_metadata VALUES (?, ?)').run('snapshot', JSON.stringify(context.plan.metadata));
|
|
24
|
+
const insert = database.prepare('INSERT INTO georeference_entities (id, entity_type, name, country_id, parent_id, codes, names, level, coordinates, boundary) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
|
25
|
+
for (const original of context.plan.entities()) {
|
|
26
|
+
context.signal?.throwIfAborted();
|
|
27
|
+
const entity = project(original, context.plan.request);
|
|
28
|
+
insert.run(entity.id, entity.entityType, entity.name, entity.countryId, entity.parentId, JSON.stringify(entity.codes ?? {}), JSON.stringify(entity.names ?? {}), entity.level ?? null, entity.coordinates == null ? null : JSON.stringify(entity.coordinates), entity.boundary == null ? null : JSON.stringify(entity.boundary));
|
|
29
|
+
}
|
|
30
|
+
database.exec('COMMIT;');
|
|
31
|
+
if (database.prepare('PRAGMA foreign_key_check').all().length)
|
|
32
|
+
throw new Error('SQLite foreign-key verification failed');
|
|
33
|
+
if (database.prepare('PRAGMA integrity_check').get()?.integrity_check !== 'ok')
|
|
34
|
+
throw new Error('SQLite integrity verification failed');
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
try {
|
|
38
|
+
database.exec('ROLLBACK');
|
|
39
|
+
}
|
|
40
|
+
catch { /* A failed COMMIT may already have rolled back. */ }
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
database.close();
|
|
45
|
+
}
|
|
46
|
+
return ['entities.sqlite'];
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=sqlite.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite.js","sourceRoot":"","sources":["../../src/adapters/sqlite.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEvD,MAAM,CAAC,MAAM,aAAa,GAAkB;IAC1C,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,eAAe;IAC7D,KAAK,CAAC,KAAK,CAAC,OAAO;QACjB,IAAI,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,qHAAqH,CAAC,CAAC;QACnL,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;QACjC,MAAM,QAAQ,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAC,CAAC;QAC9E,IAAI,CAAC;YACH,QAAQ,CAAC,IAAI,CAAC;;;;;;;;;;eAUL,CAAC,CAAC;YACX,QAAQ,CAAC,OAAO,CAAC,iDAAiD,CAAC,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC3H,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,oKAAoK,CAAC,CAAC;YACtM,KAAK,MAAM,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC;gBAC/C,OAAO,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;gBACjC,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACvD,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,QAAQ,EACrF,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,MAAM,CAAC,KAAK,IAAI,IAAI,EAC5F,MAAM,CAAC,WAAW,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC9I,CAAC;YACD,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACzB,IAAI,QAAQ,CAAC,OAAO,CAAC,0BAA0B,CAAC,CAAC,GAAG,EAAE,CAAC,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;YACzH,IAAI,QAAQ,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC,GAAG,EAAE,EAAE,eAAe,KAAK,IAAI;gBAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;QAC1I,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC;gBAAC,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,mDAAmD,CAAC,CAAC;YAChG,MAAM,KAAK,CAAC;QACd,CAAC;gBAAS,CAAC;YAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QAAC,CAAC;QAC/B,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAC7B,CAAC;CACF,CAAC","sourcesContent":["import { join } from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\nimport type { ExportAdapter } from '../types.ts';\nimport { project, validateOptions } from './common.ts';\n\nexport const sqliteAdapter: ExportAdapter = {\n format: 'sqlite', version: '1.0.0', validate: validateOptions,\n async write(context) {\n if (context.plan.externalReferences.length) throw new Error('Standalone SQLite requires included ancestors; unresolved references cannot be delivered as a ready-to-use database');\n context.signal?.throwIfAborted();\n const database = new DatabaseSync(join(context.directory, 'entities.sqlite'));\n try {\n database.exec(`PRAGMA foreign_keys = ON;\n CREATE TABLE georeference_metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);\n CREATE TABLE georeference_entities (\n id TEXT PRIMARY KEY, entity_type TEXT NOT NULL, name TEXT NOT NULL, country_id TEXT NOT NULL,\n parent_id TEXT REFERENCES georeference_entities(id) DEFERRABLE INITIALLY DEFERRED,\n codes TEXT NOT NULL DEFAULT '{}', names TEXT NOT NULL DEFAULT '{}', level INTEGER,\n coordinates TEXT, boundary TEXT\n );\n CREATE INDEX georeference_entities_parent_idx ON georeference_entities(parent_id);\n CREATE INDEX georeference_entities_country_idx ON georeference_entities(country_id);\n BEGIN;`);\n database.prepare('INSERT INTO georeference_metadata VALUES (?, ?)').run('snapshot', JSON.stringify(context.plan.metadata));\n const insert = database.prepare('INSERT INTO georeference_entities (id, entity_type, name, country_id, parent_id, codes, names, level, coordinates, boundary) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');\n for (const original of context.plan.entities()) {\n context.signal?.throwIfAborted();\n const entity = project(original, context.plan.request);\n insert.run(entity.id, entity.entityType, entity.name, entity.countryId, entity.parentId,\n JSON.stringify(entity.codes ?? {}), JSON.stringify(entity.names ?? {}), entity.level ?? null,\n entity.coordinates == null ? null : JSON.stringify(entity.coordinates), entity.boundary == null ? null : JSON.stringify(entity.boundary));\n }\n database.exec('COMMIT;');\n if (database.prepare('PRAGMA foreign_key_check').all().length) throw new Error('SQLite foreign-key verification failed');\n if (database.prepare('PRAGMA integrity_check').get()?.integrity_check !== 'ok') throw new Error('SQLite integrity verification failed');\n } catch (error) {\n try { database.exec('ROLLBACK'); } catch { /* A failed COMMIT may already have rolled back. */ }\n throw error;\n } finally { database.close(); }\n return ['entities.sqlite'];\n },\n};\n"]}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
import { Snapshot, createSnapshot } from "./snapshot.js";
|
|
7
|
+
import { exportSnapshot } from "./export.js";
|
|
8
|
+
import { verifyPackage } from "./package.js";
|
|
9
|
+
import { importGeoNames } from "./sources/geonames.js";
|
|
10
|
+
import { generateFiles } from "./generate.js";
|
|
11
|
+
const HELP = `GeoReference 1.0 — generate geographic files in your project
|
|
12
|
+
|
|
13
|
+
Quick start (no permanent project dependency):
|
|
14
|
+
npx georeference
|
|
15
|
+
npx georeference countries --country FR,CG --format csv --output ./data/countries
|
|
16
|
+
npx georeference generate --snapshot FILE --types locality --output ./data/places
|
|
17
|
+
|
|
18
|
+
Without a command, generate countries as JSON in ./data/georeference.
|
|
19
|
+
Country data is downloaded from GeoNames (CC BY 4.0). Use --source-file FILE
|
|
20
|
+
for offline countryInfo.txt input. Output is a NEW directory of standalone
|
|
21
|
+
files plus manifest.json; existing destinations are never overwritten.
|
|
22
|
+
|
|
23
|
+
Commands:
|
|
24
|
+
countries [--source-file FILE | --snapshot FILE] [export options]
|
|
25
|
+
generate [--source-file FILE | --snapshot FILE] [export options]
|
|
26
|
+
snapshot --input FILE --metadata FILE --output SNAPSHOT.sqlite
|
|
27
|
+
geonames --countries countryInfo.txt --version LABEL --output SNAPSHOT.sqlite
|
|
28
|
+
[--admin1 FILE] [--admin2 FILE] [--places FILE]
|
|
29
|
+
inspect --snapshot FILE
|
|
30
|
+
export --snapshot FILE --output DIRECTORY --format FORMAT [options]
|
|
31
|
+
verify --package DIRECTORY
|
|
32
|
+
seed --package DIRECTORY [--apply] [--create-schema] [--revision N]
|
|
33
|
+
|
|
34
|
+
Formats: json, ndjson, csv, sql-postgres, sqlite, geojson
|
|
35
|
+
Export: --country IDS_OR_CODES --subdivision IDS --types country,subdivision,locality
|
|
36
|
+
--coordinates --boundaries --no-ancestors --layout flat|hierarchical
|
|
37
|
+
--compression none|gzip|zip --schema-mode schema-and-data|data-only
|
|
38
|
+
--sql-mode upsert|insert-only --spreadsheet-safe
|
|
39
|
+
Seed: --mode upsert|insert-only --batch-size N --schema NAME --table NAME
|
|
40
|
+
--mapping FILE --allow-downgrade; DATABASE_URL in environment
|
|
41
|
+
Seeding is a read-only preview unless --apply is supplied.
|
|
42
|
+
See README.md / README.fr.md for data licensing and full guides.
|
|
43
|
+
`;
|
|
44
|
+
const stringOptions = ['input', 'metadata', 'output', 'source-file', 'countries', 'admin1', 'admin2', 'places', 'version', 'snapshot', 'format', 'country', 'subdivision', 'types', 'layout', 'compression', 'schema-mode', 'sql-mode', 'package', 'mode', 'batch-size', 'revision', 'schema', 'table', 'mapping'];
|
|
45
|
+
const booleanOptions = ['help', 'coordinates', 'boundaries', 'no-ancestors', 'spreadsheet-safe', 'apply', 'create-schema', 'allow-downgrade'];
|
|
46
|
+
const supported = {
|
|
47
|
+
snapshot: ['input', 'metadata', 'output'], geonames: ['countries', 'admin1', 'admin2', 'places', 'version', 'output'], inspect: ['snapshot'], verify: ['package'],
|
|
48
|
+
export: ['snapshot', 'output', 'format', 'country', 'subdivision', 'types', 'coordinates', 'boundaries', 'no-ancestors', 'layout', 'compression', 'schema-mode', 'sql-mode', 'spreadsheet-safe'],
|
|
49
|
+
seed: ['package', 'apply', 'create-schema', 'revision', 'mode', 'batch-size', 'schema', 'table', 'mapping', 'allow-downgrade']
|
|
50
|
+
};
|
|
51
|
+
supported.generate = [...supported.export, 'source-file'];
|
|
52
|
+
supported.countries = supported.generate.filter(key => key !== 'types' && key !== 'subdivision');
|
|
53
|
+
async function* inputEntities(path, signal) {
|
|
54
|
+
if (path.endsWith('.json')) {
|
|
55
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
56
|
+
const entities = Array.isArray(parsed) ? parsed : parsed?.entities;
|
|
57
|
+
if (!Array.isArray(entities))
|
|
58
|
+
throw new Error('JSON input requires an entities array');
|
|
59
|
+
for (const entity of entities) {
|
|
60
|
+
signal.throwIfAborted();
|
|
61
|
+
yield entity;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
const stream = createReadStream(path, { encoding: 'utf8' });
|
|
66
|
+
const lines = createInterface({ input: stream, crlfDelay: Infinity });
|
|
67
|
+
try {
|
|
68
|
+
for await (const line of lines) {
|
|
69
|
+
signal.throwIfAborted();
|
|
70
|
+
if (line.trim())
|
|
71
|
+
yield JSON.parse(line);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
lines.close();
|
|
76
|
+
stream.destroy();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function main() {
|
|
81
|
+
const args = process.argv.slice(2);
|
|
82
|
+
if (args[0] === '--help' || args[0] === 'help') {
|
|
83
|
+
console.log(HELP);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const command = !args.length || args[0].startsWith('-') ? 'countries' : args.shift();
|
|
87
|
+
if (!supported[command])
|
|
88
|
+
throw new Error(`Unknown command: ${command}`);
|
|
89
|
+
const options = {};
|
|
90
|
+
for (const key of stringOptions)
|
|
91
|
+
options[key] = { type: 'string' };
|
|
92
|
+
for (const key of booleanOptions)
|
|
93
|
+
options[key] = { type: 'boolean' };
|
|
94
|
+
const { values } = parseArgs({ args, options, strict: true });
|
|
95
|
+
if (values.help) {
|
|
96
|
+
console.log(HELP);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const key of Object.keys(values))
|
|
100
|
+
if (!supported[command].includes(key))
|
|
101
|
+
throw new Error(`Option --${key} is not supported by ${command}`);
|
|
102
|
+
const string = (key, required = false) => {
|
|
103
|
+
const value = values[key];
|
|
104
|
+
if (required && (typeof value !== 'string' || !value.trim()))
|
|
105
|
+
throw new Error(`${command} requires --${key}`);
|
|
106
|
+
return typeof value === 'string' ? value : undefined;
|
|
107
|
+
};
|
|
108
|
+
const list = (key) => string(key)?.split(',').map(item => item.trim()).filter(Boolean);
|
|
109
|
+
const number = (key) => { const value = string(key); if (value === undefined)
|
|
110
|
+
return undefined; if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
|
|
111
|
+
throw new Error(`--${key} requires a nonnegative integer`); return Number(value); };
|
|
112
|
+
const exportRequest = (format) => {
|
|
113
|
+
const request = { format };
|
|
114
|
+
for (const [flag, key] of [['country', 'countries'], ['subdivision', 'subdivisions'], ['types', 'entityTypes']])
|
|
115
|
+
if (string(flag) !== undefined)
|
|
116
|
+
request[key] = list(flag);
|
|
117
|
+
for (const [flag, key] of [['layout', 'layout'], ['compression', 'compression'], ['schema-mode', 'schemaMode'], ['sql-mode', 'sqlMode']])
|
|
118
|
+
if (string(flag) !== undefined)
|
|
119
|
+
request[key] = string(flag);
|
|
120
|
+
if (values.coordinates)
|
|
121
|
+
request.coordinates = true;
|
|
122
|
+
if (values.boundaries)
|
|
123
|
+
request.boundaries = true;
|
|
124
|
+
if (values['no-ancestors'])
|
|
125
|
+
request.includeAncestors = false;
|
|
126
|
+
if (values['spreadsheet-safe'])
|
|
127
|
+
request.spreadsheetSafe = true;
|
|
128
|
+
return request;
|
|
129
|
+
};
|
|
130
|
+
const controller = new AbortController();
|
|
131
|
+
const abort = () => controller.abort();
|
|
132
|
+
process.once('SIGINT', abort);
|
|
133
|
+
try {
|
|
134
|
+
let result;
|
|
135
|
+
if (command === 'countries' || command === 'generate') {
|
|
136
|
+
const request = exportRequest(string('format') ?? 'json');
|
|
137
|
+
if (command === 'countries')
|
|
138
|
+
request.entityTypes = ['country'];
|
|
139
|
+
result = await generateFiles({ output: string('output') ?? 'data/georeference', request,
|
|
140
|
+
...(string('snapshot') ? { snapshot: string('snapshot') } : {}), ...(string('source-file') ? { sourceFile: string('source-file') } : {}), signal: controller.signal });
|
|
141
|
+
}
|
|
142
|
+
else if (command === 'snapshot') {
|
|
143
|
+
const metadata = JSON.parse(await readFile(string('metadata', true), 'utf8'));
|
|
144
|
+
const output = string('output', true);
|
|
145
|
+
await createSnapshot(output, metadata, inputEntities(string('input', true), controller.signal));
|
|
146
|
+
const snapshot = Snapshot.open(output);
|
|
147
|
+
try {
|
|
148
|
+
result = { path: output, count: snapshot.count, datasetVersion: snapshot.metadata.datasetVersion };
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
snapshot.close();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else if (command === 'geonames') {
|
|
155
|
+
const paths = { countryInfo: string('countries', true), ...(string('admin1') ? { admin1: string('admin1') } : {}), ...(string('admin2') ? { admin2: string('admin2') } : {}), ...(string('places') ? { places: string('places') } : {}) };
|
|
156
|
+
const imported = await importGeoNames(paths, { datasetVersion: string('version', true) });
|
|
157
|
+
await createSnapshot(string('output', true), imported.metadata, imported.entities);
|
|
158
|
+
result = { count: imported.entities.length, metadata: imported.metadata };
|
|
159
|
+
}
|
|
160
|
+
else if (command === 'export' || command === 'inspect') {
|
|
161
|
+
const snapshot = Snapshot.open(string('snapshot', true));
|
|
162
|
+
try {
|
|
163
|
+
if (command === 'inspect') {
|
|
164
|
+
const counts = { country: 0, subdivision: 0, locality: 0 };
|
|
165
|
+
for (const entity of snapshot.entities())
|
|
166
|
+
counts[entity.entityType]++;
|
|
167
|
+
result = { metadata: snapshot.metadata, count: snapshot.count, counts };
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const request = exportRequest(string('format', true));
|
|
171
|
+
result = await exportSnapshot(snapshot, string('output', true), request, { signal: controller.signal });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
snapshot.close();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (command === 'verify')
|
|
179
|
+
result = await verifyPackage(string('package', true));
|
|
180
|
+
else if (command === 'seed') {
|
|
181
|
+
const packageDirectory = string('package', true);
|
|
182
|
+
// Reject corrupted or unsupported packages before connecting to the database.
|
|
183
|
+
const { readPackage } = await import("./package.js");
|
|
184
|
+
const input = await readPackage(packageDirectory);
|
|
185
|
+
const connectionString = process.env.DATABASE_URL;
|
|
186
|
+
if (!connectionString)
|
|
187
|
+
throw new Error('DATABASE_URL is required for seed');
|
|
188
|
+
const { Client } = await import('pg');
|
|
189
|
+
const { seedEntities } = await import("./seeding/index.js");
|
|
190
|
+
const client = new Client({ connectionString });
|
|
191
|
+
try {
|
|
192
|
+
await client.connect();
|
|
193
|
+
const mapping = string('mapping') ? JSON.parse(await readFile(string('mapping'), 'utf8')) : {};
|
|
194
|
+
if (string('schema'))
|
|
195
|
+
mapping.schema = string('schema');
|
|
196
|
+
if (string('table'))
|
|
197
|
+
mapping.table = string('table');
|
|
198
|
+
const seedOptions = { dryRun: !values.apply, createSchema: Boolean(values['create-schema']), allowDowngrade: Boolean(values['allow-downgrade']), mapping, signal: controller.signal };
|
|
199
|
+
if (string('mode'))
|
|
200
|
+
seedOptions.mode = string('mode');
|
|
201
|
+
const batchSize = number('batch-size'), revision = number('revision');
|
|
202
|
+
if (batchSize !== undefined)
|
|
203
|
+
seedOptions.batchSize = batchSize;
|
|
204
|
+
if (revision !== undefined)
|
|
205
|
+
seedOptions.revision = revision;
|
|
206
|
+
result = await seedEntities(client, input, seedOptions);
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
await client.end();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
console.log(JSON.stringify(result, null, 2));
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
process.removeListener('SIGINT', abort);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
main().catch((error) => { console.error('Error: ' + (error instanceof Error ? error.message : String(error))); process.exitCode = error instanceof Error && error.name === 'AbortError' ? 130 : 1; });
|
|
219
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\nimport { createReadStream } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { createInterface } from 'node:readline';\nimport { parseArgs } from 'node:util';\nimport { Snapshot, createSnapshot } from './snapshot.ts';\nimport { exportSnapshot } from './export.ts';\nimport { verifyPackage } from './package.ts';\nimport { importGeoNames } from './sources/geonames.ts';\nimport { generateFiles } from './generate.ts';\nimport type { Entity, ExportRequest, SnapshotMetadata } from './types.ts';\nimport type { SeedMapping, SeedOptions } from './seeding/types.ts';\n\nconst HELP=`GeoReference 1.0 — generate geographic files in your project\n\nQuick start (no permanent project dependency):\n npx georeference\n npx georeference countries --country FR,CG --format csv --output ./data/countries\n npx georeference generate --snapshot FILE --types locality --output ./data/places\n\nWithout a command, generate countries as JSON in ./data/georeference.\nCountry data is downloaded from GeoNames (CC BY 4.0). Use --source-file FILE\nfor offline countryInfo.txt input. Output is a NEW directory of standalone\nfiles plus manifest.json; existing destinations are never overwritten.\n\nCommands:\n countries [--source-file FILE | --snapshot FILE] [export options]\n generate [--source-file FILE | --snapshot FILE] [export options]\n snapshot --input FILE --metadata FILE --output SNAPSHOT.sqlite\n geonames --countries countryInfo.txt --version LABEL --output SNAPSHOT.sqlite\n [--admin1 FILE] [--admin2 FILE] [--places FILE]\n inspect --snapshot FILE\n export --snapshot FILE --output DIRECTORY --format FORMAT [options]\n verify --package DIRECTORY\n seed --package DIRECTORY [--apply] [--create-schema] [--revision N]\n\nFormats: json, ndjson, csv, sql-postgres, sqlite, geojson\nExport: --country IDS_OR_CODES --subdivision IDS --types country,subdivision,locality\n --coordinates --boundaries --no-ancestors --layout flat|hierarchical\n --compression none|gzip|zip --schema-mode schema-and-data|data-only\n --sql-mode upsert|insert-only --spreadsheet-safe\nSeed: --mode upsert|insert-only --batch-size N --schema NAME --table NAME\n --mapping FILE --allow-downgrade; DATABASE_URL in environment\nSeeding is a read-only preview unless --apply is supplied.\nSee README.md / README.fr.md for data licensing and full guides.\n`;\nconst stringOptions=['input','metadata','output','source-file','countries','admin1','admin2','places','version','snapshot','format','country','subdivision','types','layout','compression','schema-mode','sql-mode','package','mode','batch-size','revision','schema','table','mapping'];\nconst booleanOptions=['help','coordinates','boundaries','no-ancestors','spreadsheet-safe','apply','create-schema','allow-downgrade'];\nconst supported:Record<string,string[]>={\n snapshot:['input','metadata','output'],geonames:['countries','admin1','admin2','places','version','output'],inspect:['snapshot'],verify:['package'],\n export:['snapshot','output','format','country','subdivision','types','coordinates','boundaries','no-ancestors','layout','compression','schema-mode','sql-mode','spreadsheet-safe'],\n seed:['package','apply','create-schema','revision','mode','batch-size','schema','table','mapping','allow-downgrade']\n};\nsupported.generate=[...supported.export!,'source-file'];\nsupported.countries=supported.generate.filter(key=>key!=='types'&&key!=='subdivision');\nasync function* inputEntities(path:string,signal:AbortSignal):AsyncIterable<Entity> {\n if(path.endsWith('.json')) {\n const parsed=JSON.parse(await readFile(path,'utf8')) as unknown;\n const entities=Array.isArray(parsed)?parsed:(parsed as {entities?:unknown[]})?.entities;\n if(!Array.isArray(entities)) throw new Error('JSON input requires an entities array');\n for(const entity of entities) {signal.throwIfAborted();yield entity as Entity;}\n } else {\n const stream=createReadStream(path,{encoding:'utf8'});\n const lines=createInterface({input:stream,crlfDelay:Infinity});\n try {for await(const line of lines) {signal.throwIfAborted();if(line.trim()) yield JSON.parse(line) as Entity;}}\n finally {lines.close();stream.destroy();}\n }\n}\nasync function main():Promise<void> {\n const args=process.argv.slice(2);\n if(args[0]==='--help'||args[0]==='help') {console.log(HELP);return;}\n const command=!args.length||args[0]!.startsWith('-')?'countries':args.shift()!;\n if(!supported[command]) throw new Error(`Unknown command: ${command}`);\n const options:Record<string,{type:'string'|'boolean'}>={};\n for(const key of stringOptions) options[key]={type:'string'};\n for(const key of booleanOptions) options[key]={type:'boolean'};\n const {values}=parseArgs({args,options,strict:true});\n if(values.help) {console.log(HELP);return;}\n for(const key of Object.keys(values)) if(!supported[command]!.includes(key)) throw new Error(`Option --${key} is not supported by ${command}`);\n const string=(key:string,required=false):string|undefined=>{\n const value=values[key];\n if(required&&(typeof value!=='string'||!value.trim())) throw new Error(`${command} requires --${key}`);\n return typeof value==='string'?value:undefined;\n };\n const list=(key:string)=>string(key)?.split(',').map(item=>item.trim()).filter(Boolean);\n const number=(key:string)=>{const value=string(key);if(value===undefined)return undefined;if(!/^\\d+$/.test(value)||!Number.isSafeInteger(Number(value))) throw new Error(`--${key} requires a nonnegative integer`);return Number(value);};\n const exportRequest=(format:string):ExportRequest=>{\n const request:ExportRequest={format};\n for(const [flag,key] of [['country','countries'],['subdivision','subdivisions'],['types','entityTypes']] as const) if(string(flag)!==undefined) (request as unknown as Record<string,unknown>)[key]=list(flag);\n for(const [flag,key] of [['layout','layout'],['compression','compression'],['schema-mode','schemaMode'],['sql-mode','sqlMode']] as const) if(string(flag)!==undefined) (request as unknown as Record<string,unknown>)[key]=string(flag);\n if(values.coordinates)request.coordinates=true;if(values.boundaries)request.boundaries=true;if(values['no-ancestors'])request.includeAncestors=false;if(values['spreadsheet-safe'])request.spreadsheetSafe=true;\n return request;\n };\n const controller=new AbortController();const abort=()=>controller.abort();process.once('SIGINT',abort);\n try {\n let result:unknown;\n if(command==='countries'||command==='generate') {\n const request=exportRequest(string('format')??'json');\n if(command==='countries') request.entityTypes=['country'];\n result=await generateFiles({output:string('output')??'data/georeference',request,\n ...(string('snapshot')?{snapshot:string('snapshot')!}:{}),...(string('source-file')?{sourceFile:string('source-file')!}:{}),signal:controller.signal});\n } else if(command==='snapshot') {\n const metadata=JSON.parse(await readFile(string('metadata',true)!,'utf8')) as SnapshotMetadata;\n const output=string('output',true)!;\n await createSnapshot(output,metadata,inputEntities(string('input',true)!,controller.signal));\n const snapshot=Snapshot.open(output);try{result={path:output,count:snapshot.count,datasetVersion:snapshot.metadata.datasetVersion};}finally{snapshot.close();}\n } else if(command==='geonames') {\n const paths={countryInfo:string('countries',true)!,...(string('admin1')?{admin1:string('admin1')!}:{}),...(string('admin2')?{admin2:string('admin2')!}:{}),...(string('places')?{places:string('places')!}:{})};\n const imported=await importGeoNames(paths,{datasetVersion:string('version',true)!});\n await createSnapshot(string('output',true)!,imported.metadata,imported.entities);\n result={count:imported.entities.length,metadata:imported.metadata};\n } else if(command==='export'||command==='inspect') {\n const snapshot=Snapshot.open(string('snapshot',true)!);\n try {\n if(command==='inspect') {\n const counts={country:0,subdivision:0,locality:0};for(const entity of snapshot.entities())counts[entity.entityType]++;\n result={metadata:snapshot.metadata,count:snapshot.count,counts};\n } else {\n const request=exportRequest(string('format',true)!);\n result=await exportSnapshot(snapshot,string('output',true)!,request,{signal:controller.signal});\n }\n } finally{snapshot.close();}\n } else if(command==='verify') result=await verifyPackage(string('package',true)!);\n else if(command==='seed') {\n const packageDirectory=string('package',true)!;\n // Reject corrupted or unsupported packages before connecting to the database.\n const {readPackage}=await import('./package.ts');const input=await readPackage(packageDirectory);\n const connectionString=process.env.DATABASE_URL;if(!connectionString) throw new Error('DATABASE_URL is required for seed');\n const {Client}=await import('pg');const {seedEntities}=await import('./seeding/index.ts');\n const client=new Client({connectionString});\n try {\n await client.connect();\n const mapping:SeedMapping=string('mapping')?JSON.parse(await readFile(string('mapping')!,'utf8')) as SeedMapping:{};\n if(string('schema'))mapping.schema=string('schema')!;if(string('table'))mapping.table=string('table')!;\n const seedOptions:SeedOptions={dryRun:!values.apply,createSchema:Boolean(values['create-schema']),allowDowngrade:Boolean(values['allow-downgrade']),mapping,signal:controller.signal};\n if(string('mode'))seedOptions.mode=string('mode') as 'upsert'|'insert-only';\n const batchSize=number('batch-size'),revision=number('revision');if(batchSize!==undefined)seedOptions.batchSize=batchSize;if(revision!==undefined)seedOptions.revision=revision;\n result=await seedEntities(client,input,seedOptions);\n } finally {await client.end();}\n }\n console.log(JSON.stringify(result,null,2));\n } finally {process.removeListener('SIGINT',abort);}\n}\nmain().catch((error:unknown)=>{console.error('Error: '+(error instanceof Error?error.message:String(error)));process.exitCode=error instanceof Error&&error.name==='AbortError'?130:1;});\n"]}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Snapshot } from './snapshot.ts';
|
|
2
|
+
import type { Entity } from './types.ts';
|
|
3
|
+
export declare function createClient(snapshot: Snapshot): {
|
|
4
|
+
getEntity(id: string): Entity | undefined;
|
|
5
|
+
getCountry(id: string): Entity | undefined;
|
|
6
|
+
listCountries(): Entity[];
|
|
7
|
+
listSubdivisions(countryId?: string, parentId?: string): Entity[];
|
|
8
|
+
iterateLocalities(countryId?: string): IterableIterator<Entity>;
|
|
9
|
+
};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function createClient(snapshot) {
|
|
2
|
+
function* select(entityType, countryId) {
|
|
3
|
+
const plan = snapshot.select({ entityTypes: [entityType], includeAncestors: false, coordinates: true, boundaries: true, ...(countryId ? { countries: [countryId] } : {}) });
|
|
4
|
+
try {
|
|
5
|
+
yield* plan.entities();
|
|
6
|
+
}
|
|
7
|
+
finally {
|
|
8
|
+
plan.dispose?.();
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
getEntity(id) { return snapshot.get(id); },
|
|
13
|
+
getCountry(id) {
|
|
14
|
+
const direct = snapshot.get(id);
|
|
15
|
+
if (direct)
|
|
16
|
+
return direct.entityType === 'country' ? direct : undefined;
|
|
17
|
+
return [...select('country')].find(entity => entity.codes?.iso2?.toUpperCase() === id.toUpperCase() || entity.codes?.iso3?.toUpperCase() === id.toUpperCase());
|
|
18
|
+
},
|
|
19
|
+
listCountries() { return [...select('country')]; },
|
|
20
|
+
listSubdivisions(countryId, parentId) { return [...select('subdivision', countryId)].filter(entity => parentId === undefined || entity.parentId === parentId); },
|
|
21
|
+
*iterateLocalities(countryId) { yield* select('locality', countryId); }
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,YAAY,CAAC,QAAiB;IAC5C,QAAQ,CAAC,CAAC,MAAM,CAAC,UAA6C,EAAC,SAAiB;QAC9E,MAAM,IAAI,GAAC,QAAQ,CAAC,MAAM,CAAC,EAAC,WAAW,EAAC,CAAC,UAAU,CAAC,EAAC,gBAAgB,EAAC,KAAK,EAAC,WAAW,EAAC,IAAI,EAAC,UAAU,EAAC,IAAI,EAAC,GAAG,CAAC,SAAS,CAAA,CAAC,CAAA,EAAC,SAAS,EAAC,CAAC,SAAS,CAAC,EAAC,CAAA,CAAC,CAAA,EAAE,CAAC,EAAC,CAAC,CAAC;QACzJ,IAAI,CAAC;YAAA,KAAK,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;QAAA,CAAC;gBAAS,CAAC;YAAA,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QAAA,CAAC;IAC3D,CAAC;IACD,OAAO;QACL,SAAS,CAAC,EAAS,IAAqB,OAAO,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAClE,UAAU,CAAC,EAAS;YAClB,MAAM,MAAM,GAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC9B,IAAG,MAAM;gBAAE,OAAO,MAAM,CAAC,UAAU,KAAG,SAAS,CAAA,CAAC,CAAA,MAAM,CAAA,CAAC,CAAA,SAAS,CAAC;YACjE,OAAO,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAA,EAAE,CAAA,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,KAAG,EAAE,CAAC,WAAW,EAAE,IAAE,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,KAAG,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;QACzJ,CAAC;QACD,aAAa,KAAc,OAAO,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3D,gBAAgB,CAAC,SAAiB,EAAC,QAAgB,IAAa,OAAO,CAAC,GAAG,MAAM,CAAC,aAAa,EAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAA,EAAE,CAAA,QAAQ,KAAG,SAAS,IAAE,MAAM,CAAC,QAAQ,KAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC/K,CAAC,iBAAiB,CAAC,SAAiB,IAA6B,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,EAAC,SAAS,CAAC,CAAC,CAAC,CAAC;KACxG,CAAC;AACJ,CAAC","sourcesContent":["import type { Snapshot } from './snapshot.ts';\nimport type { Entity } from './types.ts';\nexport function createClient(snapshot:Snapshot) {\n function* select(entityType:'country'|'subdivision'|'locality',countryId?:string):IterableIterator<Entity> {\n const plan=snapshot.select({entityTypes:[entityType],includeAncestors:false,coordinates:true,boundaries:true,...(countryId?{countries:[countryId]}:{})});\n try {yield* plan.entities();} finally {plan.dispose?.();}\n }\n return {\n getEntity(id:string):Entity|undefined { return snapshot.get(id); },\n getCountry(id:string):Entity|undefined {\n const direct=snapshot.get(id);\n if(direct) return direct.entityType==='country'?direct:undefined;\n return [...select('country')].find(entity=>entity.codes?.iso2?.toUpperCase()===id.toUpperCase()||entity.codes?.iso3?.toUpperCase()===id.toUpperCase());\n },\n listCountries():Entity[] { return [...select('country')]; },\n listSubdivisions(countryId?:string,parentId?:string):Entity[] { return [...select('subdivision',countryId)].filter(entity=>parentId===undefined||entity.parentId===parentId); },\n *iterateLocalities(countryId?:string):IterableIterator<Entity> { yield* select('locality',countryId); }\n };\n}\n"]}
|
package/dist/export.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ExportAdapter, ExportManifest, ExportRequest } from './types.ts';
|
|
2
|
+
import type { Snapshot } from './snapshot.ts';
|
|
3
|
+
export declare function registerExportAdapter(adapter: ExportAdapter): void;
|
|
4
|
+
export declare function exportSnapshot(snapshot: Snapshot, directory: string, request: ExportRequest, options?: {
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
}): Promise<ExportManifest>;
|
package/dist/export.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
3
|
+
import { access, mkdir, mkdtemp, rename, rm, writeFile, unlink } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
import { createGzip } from 'node:zlib';
|
|
7
|
+
import yazl from 'yazl';
|
|
8
|
+
import { canonicalJson } from "./validation.js";
|
|
9
|
+
import { fileDigest, packageFile } from "./files.js";
|
|
10
|
+
const extensions = new Map();
|
|
11
|
+
const builtins = ['json', 'ndjson', 'csv', 'sql-postgres', 'sqlite', 'geojson'];
|
|
12
|
+
async function writeManifest(directory, manifest) {
|
|
13
|
+
const text = JSON.stringify(manifest, null, 2) + '\n';
|
|
14
|
+
if (Buffer.byteLength(text) > 1024 * 1024)
|
|
15
|
+
throw new Error('Manifest exceeds 1 MiB limit; split the export scope');
|
|
16
|
+
await writeFile(join(directory, 'manifest.json'), text);
|
|
17
|
+
}
|
|
18
|
+
export function registerExportAdapter(adapter) {
|
|
19
|
+
if (!adapter.format || !adapter.version || builtins.includes(adapter.format) || extensions.has(adapter.format))
|
|
20
|
+
throw new Error('Adapter format must be unique and versioned');
|
|
21
|
+
extensions.set(adapter.format, adapter);
|
|
22
|
+
}
|
|
23
|
+
function normalize(request) {
|
|
24
|
+
const allowed = new Set(['format', 'layout', 'compression', 'countries', 'subdivisions', 'entityTypes', 'includeAncestors', 'coordinates', 'boundaries', 'schemaMode', 'sqlMode', 'spreadsheetSafe']);
|
|
25
|
+
for (const key of Object.keys(request))
|
|
26
|
+
if (!allowed.has(key))
|
|
27
|
+
throw new Error(`Unknown export option: ${key}`);
|
|
28
|
+
if (!['none', 'gzip', 'zip'].includes(request.compression ?? 'none'))
|
|
29
|
+
throw new Error('Unsupported compression');
|
|
30
|
+
for (const [key, allowedValues] of Object.entries({ layout: ['flat', 'hierarchical'], schemaMode: ['schema-and-data', 'data-only'], sqlMode: ['upsert', 'insert-only'] })) {
|
|
31
|
+
const value = request[key];
|
|
32
|
+
if (value !== undefined && !allowedValues.includes(value))
|
|
33
|
+
throw new Error(`Invalid ${key}`);
|
|
34
|
+
}
|
|
35
|
+
for (const key of ['includeAncestors', 'coordinates', 'boundaries', 'spreadsheetSafe'])
|
|
36
|
+
if (request[key] !== undefined && typeof request[key] !== 'boolean')
|
|
37
|
+
throw new Error(`Invalid boolean option: ${key}`);
|
|
38
|
+
const result = { ...request, layout: request.layout ?? 'flat', compression: request.compression ?? 'none', includeAncestors: request.includeAncestors ?? true, coordinates: request.coordinates ?? false, boundaries: request.boundaries ?? false };
|
|
39
|
+
for (const key of ['countries', 'subdivisions', 'entityTypes']) {
|
|
40
|
+
if (request[key] !== undefined) {
|
|
41
|
+
if (!Array.isArray(request[key]) || request[key].some(value => typeof value !== 'string' || !value))
|
|
42
|
+
throw new Error(`Invalid ${key}`);
|
|
43
|
+
result[key] = [...new Set(request[key])].sort();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
export async function exportSnapshot(snapshot, directory, request, options = {}) {
|
|
49
|
+
options.signal?.throwIfAborted();
|
|
50
|
+
const normalized = normalize(request);
|
|
51
|
+
const adapters = await import("./adapters/index.js");
|
|
52
|
+
const adapter = extensions.get(normalized.format) ?? adapters.getBuiltinAdapters().find(item => item.format === normalized.format);
|
|
53
|
+
if (!adapter)
|
|
54
|
+
throw new Error(`Unsupported export format: ${normalized.format}`);
|
|
55
|
+
adapter.validate(normalized);
|
|
56
|
+
await mkdir(dirname(directory), { recursive: true });
|
|
57
|
+
try {
|
|
58
|
+
await access(directory);
|
|
59
|
+
throw new Error('Export destination already exists');
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
if (error.code !== 'ENOENT')
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
const temporary = await mkdtemp(join(dirname(directory), '.' + basename(directory) + '-'));
|
|
66
|
+
let disposePlan;
|
|
67
|
+
try {
|
|
68
|
+
const plan = snapshot.select(normalized);
|
|
69
|
+
disposePlan = plan.dispose;
|
|
70
|
+
const hash = createHash('sha256').update(canonicalJson({ metadata: plan.metadata, request: normalized }));
|
|
71
|
+
const counts = { country: 0, subdivision: 0, locality: 0 };
|
|
72
|
+
let missingCoordinates = 0, missingBoundaries = 0;
|
|
73
|
+
for (const entity of (plan.identityEntities?.() ?? plan.entities())) {
|
|
74
|
+
options.signal?.throwIfAborted();
|
|
75
|
+
hash.update('\n' + canonicalJson(entity));
|
|
76
|
+
counts[entity.entityType]++;
|
|
77
|
+
if (normalized.coordinates && !entity.coordinates)
|
|
78
|
+
missingCoordinates++;
|
|
79
|
+
if (normalized.boundaries && !entity.boundary)
|
|
80
|
+
missingBoundaries++;
|
|
81
|
+
}
|
|
82
|
+
let paths = await adapter.write({ plan, directory: temporary, ...(options.signal ? { signal: options.signal } : {}) });
|
|
83
|
+
if (!paths.length || new Set(paths).size !== paths.length || paths.includes('manifest.json'))
|
|
84
|
+
throw new Error('Adapter returned invalid file list');
|
|
85
|
+
for (const path of paths)
|
|
86
|
+
await packageFile(temporary, path);
|
|
87
|
+
if (normalized.compression === 'gzip') {
|
|
88
|
+
const compressed = [];
|
|
89
|
+
for (const path of paths) {
|
|
90
|
+
await pipeline(createReadStream(join(temporary, path)), createGzip(), createWriteStream(join(temporary, path + '.gz'), { flags: 'wx' }), { signal: options.signal });
|
|
91
|
+
await unlink(join(temporary, path));
|
|
92
|
+
compressed.push(path + '.gz');
|
|
93
|
+
}
|
|
94
|
+
paths = compressed;
|
|
95
|
+
}
|
|
96
|
+
const manifest = { contractVersion: '1.0', generatorVersion: '1.0.0', adapterVersion: adapter.version, datasetVersion: plan.metadata.datasetVersion,
|
|
97
|
+
contentId: hash.digest('hex'), request: normalized, sources: plan.metadata.sources, count: plan.count, counts, contextIds: plan.contextIds, externalReferences: plan.externalReferences,
|
|
98
|
+
geometry: { coordinates: { requested: normalized.coordinates, missing: missingCoordinates }, boundaries: { requested: normalized.boundaries, missing: missingBoundaries }, crs: 'OGC:CRS84' },
|
|
99
|
+
scope: { kind: 'full', complete: plan.count === snapshot.count && normalized.includeAncestors !== false }, files: [] };
|
|
100
|
+
for (const path of paths.sort())
|
|
101
|
+
manifest.files.push({ path, ...await fileDigest(join(temporary, path)) });
|
|
102
|
+
await writeManifest(temporary, manifest);
|
|
103
|
+
if (normalized.compression === 'zip') {
|
|
104
|
+
const zip = new yazl.ZipFile();
|
|
105
|
+
for (const path of [...paths, 'manifest.json'])
|
|
106
|
+
zip.addFile(join(temporary, path), path, { mtime: new Date('1980-01-01T00:00:00Z') });
|
|
107
|
+
const finish = pipeline(zip.outputStream, createWriteStream(join(temporary, 'package.zip'), { flags: 'wx' }), { signal: options.signal });
|
|
108
|
+
zip.end();
|
|
109
|
+
await finish;
|
|
110
|
+
for (const path of paths)
|
|
111
|
+
await unlink(join(temporary, path));
|
|
112
|
+
manifest.files = [{ path: 'package.zip', ...await fileDigest(join(temporary, 'package.zip')) }];
|
|
113
|
+
await writeManifest(temporary, manifest);
|
|
114
|
+
}
|
|
115
|
+
options.signal?.throwIfAborted();
|
|
116
|
+
await rename(temporary, directory);
|
|
117
|
+
return manifest;
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
await rm(temporary, { recursive: true, force: true });
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
disposePlan?.();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=export.js.map
|