@nspot/geo-engine 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -23
- package/dist/cli.js +2 -2
- package/dist/packs.d.ts +16 -1
- package/dist/packs.js +55 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -55,7 +55,17 @@ import { installPack } from "@nspot/geo-engine";
|
|
|
55
55
|
await installPack(pool, "https://example.com/packs/romania-1.0.0.json");
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
`installPack` also accepts a file path
|
|
58
|
+
`installPack` also accepts a file path, an already-parsed pack object, or a bare npm
|
|
59
|
+
package name, resolved like `require()` to `<package>/pack.json`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
await installPack(pool, "@nspot/geo-pack-romania");
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
so hosts can `npm i @nspot/geo-pack-romania` and install it without knowing a URL or file
|
|
66
|
+
path. Pass `{ resolveFrom }` to resolve the package name from a directory other than
|
|
67
|
+
`process.cwd()`. A pack package must expose `pack.json`; if its `package.json` declares
|
|
68
|
+
an `exports` map, that map must include `"./pack.json"`.
|
|
59
69
|
|
|
60
70
|
**4. Filter your own query by region.**
|
|
61
71
|
|
|
@@ -114,7 +124,7 @@ usage: geo-engine <command> [options]
|
|
|
114
124
|
register --name N --table [S.]T --id C (--geometry G | --lng X --lat Y)
|
|
115
125
|
unregister --name N
|
|
116
126
|
rebuild [--source N] recompute memberships (one source or all)
|
|
117
|
-
pack install <url|file>
|
|
127
|
+
pack install <url|file|package>
|
|
118
128
|
pack list
|
|
119
129
|
pack uninstall <slug>
|
|
120
130
|
|
|
@@ -136,7 +146,7 @@ database, and `cause` when it wraps something else (a zod `ZodError`, a fetch fa
|
|
|
136
146
|
| `invalid_input` | Postgres rejected a value (`22023`) — e.g. a malformed geometry or invalid enum value |
|
|
137
147
|
| `invalid_input` | `idType` is not a plain SQL type name (`inRegions` / `notInRegions`), raised before any SQL is built |
|
|
138
148
|
| `invalid_input` | `filter.regions` is empty in `inRegions` / `ids`, raised before the database is touched |
|
|
139
|
-
| `invalid_input` | `installPack` could not download, read or JSON-parse the pack (`cause` is the underlying error) |
|
|
149
|
+
| `invalid_input` | `installPack` could not download, read, resolve or JSON-parse the pack (`cause` is the underlying error) |
|
|
140
150
|
| `invalid_input` | the pack document failed validation — `Invalid pack document: <path>: <issue>; …`, `cause` is the `ZodError` |
|
|
141
151
|
| `invalid_input` | `migrate` found a database whose schema is newer than the one this package ships |
|
|
142
152
|
| `unknown_source` | the request named a source that isn't registered |
|
|
@@ -168,23 +178,3 @@ writing rows or editing regions at that moment. Registering a source is as power
|
|
|
168
178
|
writing a trigger by hand — only let database administrators call `registerSource` /
|
|
169
179
|
`unregisterSource` (the underlying `geo.register_source` / `geo.unregister_source` are not
|
|
170
180
|
executable by PUBLIC).
|
|
171
|
-
|
|
172
|
-
## Releasing
|
|
173
|
-
|
|
174
|
-
`pnpm sdk:rehearsal` (from the repo root) packs the SDK and installs it into a throwaway
|
|
175
|
-
project against a scratch database; run it before every release.
|
|
176
|
-
|
|
177
|
-
To cut a release: bump `version` in `packages/sdk-node/package.json`, run
|
|
178
|
-
`pnpm sdk:rehearsal` and confirm it passes, commit the version bump, tag the commit
|
|
179
|
-
`sdk-node-v<version>` (e.g. `sdk-node-v0.1.0`), and push the tag. Pushing a tag matching
|
|
180
|
-
`sdk-node-v*` runs `.github/workflows/publish-sdk.yml`, which checks the tag against
|
|
181
|
-
`package.json`'s version, builds the package, runs the full SDK test suite against a
|
|
182
|
-
PostGIS service container, and publishes to npm with provenance.
|
|
183
|
-
|
|
184
|
-
One-time setup, before the first release: create the `@nspot` organisation on npm,
|
|
185
|
-
generate an automation token scoped to it, and add that token as the repository secret
|
|
186
|
-
`NPM_TOKEN`. Without the secret the workflow is not inert: pushing a matching tag still
|
|
187
|
-
runs it, and it goes all the way through the checks, build and tests before failing at
|
|
188
|
-
the publish step. Add the secret before pushing the first tag.
|
|
189
|
-
|
|
190
|
-
A Python SDK does not exist yet.
|
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ const USAGE = `usage: geo-engine <command> [options]
|
|
|
9
9
|
register --name N --table [S.]T --id C (--geometry G | --lng X --lat Y)
|
|
10
10
|
unregister --name N
|
|
11
11
|
rebuild [--source N] recompute memberships (one source or all)
|
|
12
|
-
pack install <url|file>
|
|
12
|
+
pack install <url|file|package>
|
|
13
13
|
pack list
|
|
14
14
|
pack uninstall <slug>
|
|
15
15
|
|
|
@@ -92,7 +92,7 @@ async function run() {
|
|
|
92
92
|
else if (command === "pack") {
|
|
93
93
|
const sub = positionals[1];
|
|
94
94
|
if (sub === "install") {
|
|
95
|
-
const source = positionals[2] ?? usageError("usage: geo-engine pack install <url|file>");
|
|
95
|
+
const source = positionals[2] ?? usageError("usage: geo-engine pack install <url|file|package>");
|
|
96
96
|
const r = await installPack(pool, source);
|
|
97
97
|
console.log(`installed ${r.pack}@${r.version}: ${r.regions} regions, ${r.countries} countries, ${r.memberships} memberships`);
|
|
98
98
|
}
|
package/dist/packs.d.ts
CHANGED
|
@@ -5,9 +5,24 @@ export interface InstallPackOptions {
|
|
|
5
5
|
fetch?: typeof fetch;
|
|
6
6
|
/** Abort an http(s) pack download after this many milliseconds; defaults to 30000. */
|
|
7
7
|
timeoutMs?: number;
|
|
8
|
+
/** Directory to resolve a bare package name from; defaults to process.cwd(). */
|
|
9
|
+
resolveFrom?: string;
|
|
8
10
|
}
|
|
9
11
|
export declare function exportPack(db: Queryable, slug: string, version: string): Promise<Pack>;
|
|
10
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Install a pack into the database.
|
|
14
|
+
*
|
|
15
|
+
* `source` is a parsed pack document, or a string in one of three forms:
|
|
16
|
+
*
|
|
17
|
+
* - **an npm package name** — `"@nspot/geo-pack-romania"`, or an unscoped `"geo-pack-x"` —
|
|
18
|
+
* resolved to that package's `pack.json` the way `require()` would, starting from
|
|
19
|
+
* `opts.resolveFrom` (default: the current working directory). This is how first-party
|
|
20
|
+
* packs are distributed: `npm i @nspot/geo-pack-romania`, then pass the name.
|
|
21
|
+
* - **a file path** — `"./packs/romania/pack.json"`, relative to the current working
|
|
22
|
+
* directory, or absolute. An existing file always wins: a path that happens to look like
|
|
23
|
+
* a package name is read as a file when the file is there.
|
|
24
|
+
* - **an `http(s)` URL**, downloaded with a timeout (`opts.timeoutMs`, default 30s).
|
|
25
|
+
*/
|
|
11
26
|
export declare function installPack(db: Queryable, source: Pack | string, opts?: InstallPackOptions): Promise<InstallPackResult>;
|
|
12
27
|
/** Removes the pack's regions and the pack row; returns how many regions were removed. */
|
|
13
28
|
export declare function uninstallPack(db: Queryable, slug: string): Promise<number>;
|
package/dist/packs.js
CHANGED
|
@@ -1,12 +1,29 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
1
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import path from "node:path";
|
|
2
5
|
import { GeoEngineError, run } from "./errors.js";
|
|
3
6
|
import { packSchema } from "./types.js";
|
|
4
7
|
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
8
|
+
const PACKAGE_NAME_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
5
9
|
export async function exportPack(db, slug, version) {
|
|
6
10
|
const rows = await run(db, "SELECT geo.export_pack($1, $2) AS pack", [slug, version]);
|
|
7
11
|
return rows[0].pack;
|
|
8
12
|
}
|
|
9
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Install a pack into the database.
|
|
15
|
+
*
|
|
16
|
+
* `source` is a parsed pack document, or a string in one of three forms:
|
|
17
|
+
*
|
|
18
|
+
* - **an npm package name** — `"@nspot/geo-pack-romania"`, or an unscoped `"geo-pack-x"` —
|
|
19
|
+
* resolved to that package's `pack.json` the way `require()` would, starting from
|
|
20
|
+
* `opts.resolveFrom` (default: the current working directory). This is how first-party
|
|
21
|
+
* packs are distributed: `npm i @nspot/geo-pack-romania`, then pass the name.
|
|
22
|
+
* - **a file path** — `"./packs/romania/pack.json"`, relative to the current working
|
|
23
|
+
* directory, or absolute. An existing file always wins: a path that happens to look like
|
|
24
|
+
* a package name is read as a file when the file is there.
|
|
25
|
+
* - **an `http(s)` URL**, downloaded with a timeout (`opts.timeoutMs`, default 30s).
|
|
26
|
+
*/
|
|
10
27
|
export async function installPack(db, source, opts = {}) {
|
|
11
28
|
const raw = typeof source === "string" ? await loadPack(source, opts) : source;
|
|
12
29
|
const doc = parsePack(raw);
|
|
@@ -43,21 +60,55 @@ async function loadPack(source, opts) {
|
|
|
43
60
|
throw new GeoEngineError("invalid_input", `Pack at ${source} is not valid JSON: ${messageOf(cause)}`, { cause });
|
|
44
61
|
}
|
|
45
62
|
}
|
|
63
|
+
let file = source;
|
|
64
|
+
if (!isFile(source) && PACKAGE_NAME_RE.test(source)) {
|
|
65
|
+
try {
|
|
66
|
+
// path.resolve, not path.join: resolveFrom may be relative, and createRequire only
|
|
67
|
+
// accepts an absolute path or a file URL.
|
|
68
|
+
const req = createRequire(path.resolve(opts.resolveFrom ?? process.cwd(), "noop.js"));
|
|
69
|
+
file = req.resolve(`${source}/pack.json`);
|
|
70
|
+
}
|
|
71
|
+
catch (cause) {
|
|
72
|
+
// "pack.json" is a syntactically valid package name, so a source ending in .json that
|
|
73
|
+
// got this far was probably meant as a file path that is simply not there. Say so,
|
|
74
|
+
// rather than leaving the caller with a resolver error about a package they never had.
|
|
75
|
+
const alsoNotAFile = /\.json$/i.test(source)
|
|
76
|
+
? ` (and no file named ${path.resolve(source)} exists either)`
|
|
77
|
+
: "";
|
|
78
|
+
throw new GeoEngineError("invalid_input", `Failed to resolve pack package ${source}${alsoNotAFile}: ${messageOf(cause)}`, { cause });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
46
81
|
let text;
|
|
47
82
|
try {
|
|
48
|
-
text = await readFile(
|
|
83
|
+
text = await readFile(file, "utf8");
|
|
49
84
|
}
|
|
50
85
|
catch (cause) {
|
|
51
|
-
throw new GeoEngineError("invalid_input", `Failed to read pack file ${
|
|
86
|
+
throw new GeoEngineError("invalid_input", `Failed to read pack file ${file}: ${messageOf(cause)}`, { cause });
|
|
52
87
|
}
|
|
53
88
|
try {
|
|
54
89
|
return JSON.parse(text);
|
|
55
90
|
}
|
|
56
91
|
catch (cause) {
|
|
57
|
-
throw new GeoEngineError("invalid_input", `Pack file ${
|
|
92
|
+
throw new GeoEngineError("invalid_input", `Pack file ${file} is not valid JSON: ${messageOf(cause)}`, { cause });
|
|
58
93
|
}
|
|
59
94
|
}
|
|
60
95
|
const messageOf = (err) => (err instanceof Error ? err.message : String(err));
|
|
96
|
+
/**
|
|
97
|
+
* Does this path point at an existing file?
|
|
98
|
+
*
|
|
99
|
+
* `throwIfNoEntry: false` only covers ENOENT; a path component that is not a directory
|
|
100
|
+
* (ENOTDIR), an unreadable parent (EACCES) or a symlink loop (ELOOP) still throw. None of
|
|
101
|
+
* those is a file, and none of them should stop us trying the package-name interpretation,
|
|
102
|
+
* so every throw means "no".
|
|
103
|
+
*/
|
|
104
|
+
function isFile(p) {
|
|
105
|
+
try {
|
|
106
|
+
return statSync(p, { throwIfNoEntry: false })?.isFile() ?? false;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
61
112
|
/** Removes the pack's regions and the pack row; returns how many regions were removed. */
|
|
62
113
|
export async function uninstallPack(db, slug) {
|
|
63
114
|
const rows = await run(db, "SELECT geo.uninstall_pack($1) AS n", [slug]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nspot/geo-engine",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Region membership for your own PostGIS tables: install the geo schema, register tables, install region packs, query by region.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
|
|
12
12
|
"./types": { "types": "./dist/types.d.ts", "import": "./dist/types.js" }
|
|
13
13
|
},
|
|
14
|
-
"bin": { "geo-engine": "
|
|
14
|
+
"bin": { "geo-engine": "dist/cli.js" },
|
|
15
15
|
"files": ["dist", "sql", "README.md", "LICENSE"],
|
|
16
16
|
"engines": { "node": ">=20" },
|
|
17
17
|
"repository": {
|