diff --git a/README.md b/README.md index ae1816c3..63f2983f 100644 --- a/README.md +++ b/README.md @@ -48,17 +48,19 @@ Stac-server is a production-ready implementation of the [STAC API specification] Get started with Docker Compose for local development: ```bash -# Clone the repository +# Clone and install git clone https://github.com/stac-utils/stac-server.git cd stac-server +npm install -# Start services +# Start OpenSearch and LocalStack docker compose up -d -# Ingest sample data -npm run ingest:example +# Start the STAC API on http://localhost:3000 (leave this running) +npm run serve -# Test the API +# In a separate terminal: ingest sample data, then query the API +npm run ingest:example curl http://localhost:3000/ ``` diff --git a/bin/ingest-example.js b/bin/ingest-example.js new file mode 100755 index 00000000..f451fd81 --- /dev/null +++ b/bin/ingest-example.js @@ -0,0 +1,66 @@ +#!/usr/bin/env node +// Ingest the example collection and item into a locally running stac-server +// via the Transaction extension. Requires the API to be running (npm run serve) +// with ENABLE_TRANSACTIONS_EXTENSION=true (which `npm run serve` sets). + +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import path from 'node:path' + +const API = (process.env.STAC_API_URL || 'http://localhost:3000').replace(/\/$/, '') +const examplesDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'examples') + +const loadJson = async (file) => + JSON.parse(await readFile(path.join(examplesDir, file), 'utf8')) + +const post = async (url, body, label, { tolerate404 = false } = {}) => { + let res + try { + res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }) + } catch { + console.error(`\nCould not reach the STAC API at ${API}.`) + console.error('Make sure the services and API are running:') + console.error(' docker compose up -d # OpenSearch + LocalStack') + console.error(' npm run serve # STAC API on :3000 (in a separate terminal)\n') + process.exit(1) + } + // 409 = already exists, fine for a re-runnable example. 404 may be transient + // while a newly created collection becomes searchable; the caller retries. + if (res.status === 404 && tolerate404) return res.status + if (!res.ok && res.status !== 409) { + console.error(`Failed to ${label}: ${res.status} ${res.statusText}`) + console.error(await res.text()) + process.exit(1) + } + const note = res.status === 409 ? ' (already exists)' : '' + console.log(` ${label}: ${res.status}${note}`) + return res.status +} + +const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms) }) + +const collection = await loadJson('sample-collection.json') +const item = await loadJson('sample-item.json') + +console.log(`Ingesting example data into ${API}`) +await post(`${API}/collections`, collection, `create collection '${collection.id}'`) + +// A newly created collection isn't immediately searchable (OpenSearch refreshes +// ~1s after a write), and the item endpoint validates the collection exists, so +// retry a few times on 404 before giving up. +const itemsUrl = `${API}/collections/${collection.id}/items` +const label = `create item '${item.id}'` +/* eslint-disable no-await-in-loop -- sequential retry is intentional */ +for (let attempt = 1; ; attempt += 1) { + const status = await post(itemsUrl, item, label, { tolerate404: attempt < 10 }) + if (status !== 404) break + await sleep(1000) +} +/* eslint-enable no-await-in-loop */ + +console.log('\nDone. Try:') +console.log(` curl '${API}/search?collections=${collection.id}'`) diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index 5e516bd2..eea5d1d4 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -11,18 +11,28 @@ Get STAC Server running with sample data in 5 minutes. ## Step 1: Start the Services ```bash -# Clone the repository +# Clone and install git clone https://github.com/stac-utils/stac-server.git cd stac-server +npm install -# Start services +# Start OpenSearch and LocalStack docker compose up -d # Wait for services to be ready (about 30 seconds) sleep 30 + +# Start the STAC API (leave this running in its own terminal) +npm run serve ``` -This starts OpenSearch and the STAC API on `http://localhost:3000`. +`docker compose` starts OpenSearch and LocalStack; `npm run serve` runs the STAC API +on `http://localhost:3000` with the Transaction extension enabled (so the steps below +can create collections and items). + +> **Tip:** Once the API is running, you can load a ready-made sample collection and item +> in one step with `npm run ingest:example`, then skip to [Step 5](#step-5-search-for-items). +> The steps below walk through doing it manually. ## Step 2: Verify the API @@ -177,11 +187,10 @@ Wait for "Node started" message. ### Transaction extension errors -The Transaction extension is enabled by default in Docker Compose. To disable: - -```bash -ENABLE_TRANSACTIONS_EXTENSION=false docker compose up -``` +The Transaction extension (required for the `POST` steps and `npm run ingest:example` +above) is enabled by `npm run serve`. If you run the API another way and those +requests return 404, make sure `ENABLE_TRANSACTIONS_EXTENSION=true` is set for the +server process. ## Next Steps diff --git a/examples/sample-collection.json b/examples/sample-collection.json new file mode 100644 index 00000000..6b85335f --- /dev/null +++ b/examples/sample-collection.json @@ -0,0 +1,16 @@ +{ + "id": "sample-collection", + "type": "Collection", + "stac_version": "1.0.0", + "description": "A sample collection", + "license": "proprietary", + "extent": { + "spatial": { + "bbox": [[-180, -90, 180, 90]] + }, + "temporal": { + "interval": [["2020-01-01T00:00:00Z", null]] + } + }, + "links": [] +} diff --git a/examples/sample-item.json b/examples/sample-item.json new file mode 100644 index 00000000..c94290da --- /dev/null +++ b/examples/sample-item.json @@ -0,0 +1,23 @@ +{ + "type": "Feature", + "stac_version": "1.0.0", + "id": "sample-item", + "collection": "sample-collection", + "geometry": { + "type": "Point", + "coordinates": [-122.4, 37.8] + }, + "bbox": [-122.4, 37.8, -122.4, 37.8], + "properties": { + "datetime": "2024-01-15T10:30:00Z", + "title": "Sample Item" + }, + "assets": { + "thumbnail": { + "href": "https://example.com/thumbnail.jpg", + "type": "image/jpeg", + "title": "Thumbnail" + } + }, + "links": [] +} diff --git a/package.json b/package.json index 569bd186..ec5ca5e0 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "package": "sls package", "serve": "REQUEST_LOGGING_FORMAT=dev LOG_LEVEL=debug STAC_API_URL=http://localhost:3000 ENABLE_TRANSACTIONS_EXTENSION=true nodemon --exec node --import=tsx ./src/lambdas/api/local.ts", "build-api-docs": "npx @redocly/cli build-docs src/lambdas/api/openapi.yaml -o ./docs/api-spec.html", - "prepare": "husky" + "prepare": "husky", + "ingest:example": "node bin/ingest-example.js" }, "ava": { "verbose": true,