├── middleware ├── logger.js ├── index.js ├── authz.js ├── response-time.js ├── errors.js ├── auth.js └── cache.js ├── routes ├── admin │ ├── index.js │ └── v4 │ │ └── index.js ├── capsules │ ├── index.js │ └── v4 │ │ └── index.js ├── company │ ├── index.js │ └── v4 │ │ └── index.js ├── cores │ ├── index.js │ └── v4 │ │ └── index.js ├── crew │ ├── index.js │ └── v4 │ │ └── index.js ├── dragons │ ├── index.js │ └── v4 │ │ └── index.js ├── history │ ├── index.js │ └── v4 │ │ └── index.js ├── landpads │ ├── index.js │ └── v4 │ │ └── index.js ├── payloads │ ├── index.js │ └── v4 │ │ └── index.js ├── roadster │ ├── index.js │ └── v4 │ │ └── index.js ├── rockets │ ├── index.js │ └── v4 │ │ └── index.js ├── ships │ ├── index.js │ └── v4 │ │ └── index.js ├── starlink │ ├── index.js │ └── v4 │ │ └── index.js ├── users │ ├── index.js │ └── v4 │ │ └── index.js ├── launchpads │ ├── index.js │ └── v4 │ │ └── index.js ├── launches │ ├── index.js │ ├── v4 │ │ ├── _transform-query.js │ │ └── _transform-response.js │ └── v5 │ │ └── index.js └── index.js ├── tests └── index.test.js ├── start.sh ├── .dockerignore ├── lib ├── healthchecks │ ├── index.js │ ├── fail.js │ ├── success.js │ └── start.js ├── constants.js └── utils │ └── healthcheck.js ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── deploy.yml ├── docs ├── launches │ ├── v5 │ │ ├── README.md │ │ ├── next.md │ │ ├── latest.md │ │ ├── one.md │ │ ├── all.md │ │ └── past.md │ └── v4 │ │ ├── next.md │ │ ├── latest.md │ │ ├── one.md │ │ ├── all.md │ │ └── past.md ├── history │ └── v4 │ │ ├── schema.md │ │ ├── all.md │ │ ├── one.md │ │ └── query.md ├── crew │ └── v4 │ │ ├── schema.md │ │ ├── all.md │ │ ├── one.md │ │ └── query.md ├── capsules │ └── v4 │ │ ├── all.md │ │ ├── one.md │ │ ├── schema.md │ │ └── query.md ├── cores │ └── v4 │ │ ├── all.md │ │ ├── schema.md │ │ ├── one.md │ │ └── query.md ├── launchpads │ └── v4 │ │ ├── schema.md │ │ ├── all.md │ │ ├── one.md │ │ └── query.md ├── company │ └── v4 │ │ ├── schema.md │ │ └── all.md ├── landpads │ └── v4 │ │ ├── schema.md │ │ ├── all.md │ │ ├── one.md │ │ └── query.md ├── ships │ └── v4 │ │ ├── all.md │ │ ├── one.md │ │ ├── schema.md │ │ └── query.md ├── roadster │ └── v4 │ │ ├── schema.md │ │ ├── get.md │ │ └── query.md ├── payloads │ └── v4 │ │ ├── all.md │ │ ├── one.md │ │ ├── query.md │ │ └── schema.md ├── starlink │ └── v4 │ │ ├── all.md │ │ ├── one.md │ │ └── query.md ├── dragons │ └── v4 │ │ ├── schema.md │ │ ├── all.md │ │ └── one.md ├── clients.md └── rockets │ └── v4 │ └── all.md ├── .eslintrc.json ├── models ├── users.js ├── index.js ├── history.js ├── crew.js ├── capsules.js ├── cores.js ├── company.js ├── landpads.js ├── launchpads.js ├── roadster.js ├── ships.js ├── payloads.js └── dragons.js ├── Dockerfile ├── server.js ├── app.js ├── .gitignore ├── jobs ├── launchpads.js ├── worker.js ├── landpads.js ├── launch-library.js ├── payloads.js ├── webcast.js └── capsules.js └── package.json /middleware/logger.js: -------------------------------------------------------------------------------- 1 | import pino from 'pino'; 2 | 3 | export default pino(); 4 | -------------------------------------------------------------------------------- /routes/admin/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/capsules/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/company/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/cores/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/crew/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/dragons/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/history/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/landpads/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/payloads/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/roadster/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/rockets/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/ships/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/starlink/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/users/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /routes/launchpads/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | ]; 4 | -------------------------------------------------------------------------------- /tests/index.test.js: -------------------------------------------------------------------------------- 1 | it('should expect true to be true', () => { 2 | expect(true).toBe(true); 3 | }); 4 | -------------------------------------------------------------------------------- /routes/launches/index.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | import('./v4/index.js'), 3 | import('./v5/index.js'), 4 | ]; 5 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if [ "$SPACEX_WORKER" = "true" ]; then 4 | node ./jobs/worker.js 5 | else 6 | node ./server.js 7 | fi 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | npm-debug.log 4 | *.md 5 | test 6 | docs 7 | Dockerfile 8 | coverage 9 | LICENSE 10 | .eslintrc 11 | .gitignore 12 | .nvmrc 13 | .env -------------------------------------------------------------------------------- /lib/healthchecks/index.js: -------------------------------------------------------------------------------- 1 | export { default as fail } from './fail.js'; 2 | export { default as start } from './start.js'; 3 | export { default as success } from './success.js'; 4 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Healthchecks.io API prefix 3 | */ 4 | export const HEALTHCHECK_PREFIX = 'https://hc-ping.com'; 5 | 6 | /** 7 | * Default Port 8 | */ 9 | export const DEFAULT_PORT = 6673; 10 | -------------------------------------------------------------------------------- /middleware/index.js: -------------------------------------------------------------------------------- 1 | export { default as auth } from './auth.js'; 2 | export { default as authz } from './authz.js'; 3 | export { default as cache } from './cache.js'; 4 | export { default as errors } from './errors.js'; 5 | export { default as responseTime } from './response-time.js'; 6 | export { default as logger } from './logger.js'; 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | day: saturday 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: eslint 11 | versions: 12 | - 7.21.0 13 | - 7.24.0 14 | - dependency-name: eslint-plugin-no-secrets 15 | versions: 16 | - 0.8.9 17 | -------------------------------------------------------------------------------- /middleware/authz.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Authorization middleware 3 | * 4 | * @param {String} role Role for protected route 5 | * @returns {void} 6 | */ 7 | export default (role) => async (ctx, next) => { 8 | const { roles } = ctx.state; 9 | const allowed = roles.includes(role); 10 | if (allowed) { 11 | await next(); 12 | return; 13 | } 14 | ctx.status = 403; 15 | }; 16 | -------------------------------------------------------------------------------- /middleware/response-time.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Return header with response time 3 | * 4 | * @param {Object} ctx Koa context 5 | * @param {Function} next Next middleware 6 | * @returns {Promise} 7 | */ 8 | export default async (ctx, next) => { 9 | const start = Date.now(); 10 | await next(); 11 | const ms = Date.now() - start; 12 | ctx.set('spacex-api-response-time', `${ms}ms`); 13 | }; 14 | -------------------------------------------------------------------------------- /lib/utils/healthcheck.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import got from 'got'; 4 | 5 | const { HEALTH_URL, SPACEX_WORKER } = process.env; 6 | 7 | (async () => { 8 | try { 9 | if (SPACEX_WORKER) { 10 | process.exit(0); 11 | } 12 | await got(HEALTH_URL); 13 | process.exit(0); 14 | } catch (error) { 15 | console.log(error.message); 16 | process.exit(1); 17 | } 18 | })(); 19 | -------------------------------------------------------------------------------- /docs/launches/v5/README.md: -------------------------------------------------------------------------------- 1 | ## Changes from v4 -> v5 2 | 3 | * Crew is now an array of objects, to allow for more data on an individual launch for a crew member 4 | 5 | ### Old Format 6 | 7 | ```json 8 | { 9 | "crew": [ 10 | "1234567890", 11 | "123456789", 12 | "123456789", 13 | ] 14 | } 15 | ``` 16 | 17 | ### New Format 18 | 19 | ```json 20 | { 21 | "crew": [ 22 | "1234567890", 23 | "123456789", 24 | "123456789", 25 | ] 26 | } 27 | ``` -------------------------------------------------------------------------------- /middleware/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Error handler middleware 3 | * 4 | * @param {Object} ctx Koa context 5 | * @param {function} next Koa next function 6 | * @returns {void} 7 | */ 8 | export default async (ctx, next) => { 9 | try { 10 | await next(); 11 | } catch (err) { 12 | if (err?.kind === 'ObjectId') { 13 | err.status = 404; 14 | } else { 15 | ctx.status = err.status ?? 500; 16 | ctx.body = err.message; 17 | } 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /docs/history/v4/schema.md: -------------------------------------------------------------------------------- 1 | # History Event Schema 2 | 3 | ```json 4 | { 5 | "title": { 6 | "type": "String", 7 | "default": null, 8 | }, 9 | "event_date_utc": { 10 | "type": "String", 11 | "default": null, 12 | }, 13 | "event_date_unix": { 14 | "type": "Number", 15 | "default": null, 16 | }, 17 | "details": { 18 | "type": "String", 19 | "default": null, 20 | }, 21 | "links": { 22 | "article": { 23 | "type": "String", 24 | "default": null, 25 | }, 26 | }, 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /middleware/auth.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const db = mongoose.connection.useDb('auth', { useCache: true }); 4 | 5 | /** 6 | * Authentication middleware 7 | */ 8 | export default async (ctx, next) => { 9 | const key = ctx.request.headers['spacex-key']; 10 | if (key) { 11 | const user = await db.collection('users').findOne({ key }); 12 | if (user?.key === key) { 13 | ctx.state.roles = user.roles; 14 | await next(); 15 | return; 16 | } 17 | } 18 | ctx.status = 401; 19 | ctx.body = 'https://youtu.be/RfiQYRn7fBg'; 20 | }; 21 | -------------------------------------------------------------------------------- /docs/crew/v4/schema.md: -------------------------------------------------------------------------------- 1 | # Crew Schema 2 | 3 | ```json 4 | { 5 | "name": { 6 | "type": "String", 7 | "default": null 8 | }, 9 | "status": { 10 | "type": "String", 11 | "required": true, 12 | "enum": ["active", "inactive", "retired", "unknown"] 13 | }, 14 | "agency": { 15 | "type": "String", 16 | "default": null 17 | }, 18 | "image": { 19 | "type": "String", 20 | "default": null 21 | }, 22 | "wikipedia": { 23 | "type": "String", 24 | "default": null 25 | }, 26 | "launches": [{ 27 | "type": "UUID" 28 | }] 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /routes/admin/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { auth, authz, cache } from '../../../middleware/index.js'; 3 | 4 | const router = new Router({ 5 | prefix: '/(v4|latest)/admin', 6 | }); 7 | 8 | // Clear redis cache 9 | router.delete('/cache', auth, authz('cache:clear'), async (ctx) => { 10 | try { 11 | await cache.redis.flushall(); 12 | ctx.status = 200; 13 | } catch (error) { 14 | ctx.throw(400, error.message); 15 | } 16 | }); 17 | 18 | // Healthcheck 19 | router.get('/health', async (ctx) => { 20 | ctx.status = 200; 21 | }); 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /docs/crew/v4/all.md: -------------------------------------------------------------------------------- 1 | # Get all crew 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/crew` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "name": "Robert Behnken", 17 | "agency": "NASA", 18 | "image": "https://imgur.com/0smMgMH.png", 19 | "wikipedia": "https://en.wikipedia.org/wiki/Robert_L._Behnken", 20 | "launches": [ 21 | "5eb87d46ffd86e000604b388" 22 | ], 23 | "status": "active", 24 | "id": "5ebf1a6e23a9a60006e03a7a" 25 | }, 26 | ... 27 | ] 28 | ``` 29 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es2022": true, 5 | "jest": true 6 | }, 7 | "extends": ["airbnb-base"], 8 | "parser": "@typescript-eslint/parser", 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | }, 13 | "plugins": ["@typescript-eslint"], 14 | "overrides": [ 15 | { 16 | "files": ["tests/**"], 17 | "plugins": ["jest"], 18 | "extends": ["plugin:jest/recommended"] 19 | } 20 | ], 21 | "rules": { 22 | "no-console": 0, 23 | "import/extensions": ["error", "always"], 24 | "import/no-unresolved": ["error", { "ignore": ["^got$"] }] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /models/users.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import mongoosePaginate from 'mongoose-paginate-v2'; 3 | import idPlugin from 'mongoose-id'; 4 | 5 | const userSchema = new mongoose.Schema({ 6 | name: { 7 | type: String, 8 | default: null, 9 | }, 10 | key: { 11 | type: String, 12 | required: true, 13 | }, 14 | role: { 15 | type: String, 16 | required: true, 17 | enum: ['superuser', 'user', 'create', 'update', 'delete'], 18 | }, 19 | }, { autoCreate: true }); 20 | 21 | userSchema.plugin(mongoosePaginate); 22 | userSchema.plugin(idPlugin); 23 | 24 | const User = mongoose.model('User', userSchema); 25 | 26 | export default User; 27 | -------------------------------------------------------------------------------- /docs/capsules/v4/all.md: -------------------------------------------------------------------------------- 1 | # Get all capsules 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/capsules` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "reuse_count": 1, 17 | "water_landings": 1, 18 | "land_landings": 0, 19 | "last_update": "Reentered after three weeks in orbit", 20 | "launches": [ 21 | "5eb87cdeffd86e000604b330" 22 | ], 23 | "serial": "C101", 24 | "status": "retired", 25 | "type": "Dragon 1.0", 26 | "id": "5e9e2c5bf35918ed873b2664" 27 | }, 28 | ... 29 | ] 30 | ``` 31 | -------------------------------------------------------------------------------- /lib/healthchecks/fail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports 3 | */ 4 | import got from 'got'; 5 | import { HEALTHCHECK_PREFIX } from '../constants.js'; 6 | 7 | /** 8 | * Send fail signal to healthcheck.io 9 | * 10 | * @param {String} [id] UUID of healthcheck 11 | * @param {String|Object} [msg] Message to pass to healthcheck 12 | * @returns {Boolean} True if successful 13 | */ 14 | export default async (id = null, msg = {}) => { 15 | if (id) { 16 | const response = await got.post({ 17 | prefixUrl: HEALTHCHECK_PREFIX, 18 | url: `${id}/fail`, 19 | json: msg, 20 | }); 21 | if (response.statusCode === 200) { 22 | return true; 23 | } 24 | } 25 | return false; 26 | }; 27 | -------------------------------------------------------------------------------- /lib/healthchecks/success.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports 3 | */ 4 | import got from 'got'; 5 | import { HEALTHCHECK_PREFIX } from '../constants.js'; 6 | 7 | /** 8 | * Send start signal to healthcheck.io 9 | * 10 | * @param {String} id UUID of healthcheck 11 | * @param {String|Object} [msg] Message to pass to healthcheck 12 | * @returns {Boolean} True if successful 13 | */ 14 | export default async (id = null, msg = {}) => { 15 | if (id) { 16 | const response = await got.post({ 17 | prefixUrl: HEALTHCHECK_PREFIX, 18 | url: `${id}`, 19 | json: msg, 20 | }); 21 | if (response.statusCode === 200) { 22 | return true; 23 | } 24 | } 25 | return false; 26 | }; 27 | -------------------------------------------------------------------------------- /lib/healthchecks/start.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Imports 3 | */ 4 | import got from 'got'; 5 | import { HEALTHCHECK_PREFIX } from '../constants.js'; 6 | 7 | /** 8 | * Send start signal to healthcheck.io 9 | * 10 | * @param {String} [id] UUID of healthcheck 11 | * @param {String|Object} [msg] Message to pass to healthcheck 12 | * @returns {Boolean} True if successful 13 | */ 14 | export default async (id = null, msg = {}) => { 15 | if (id) { 16 | const response = await got.post({ 17 | prefixUrl: HEALTHCHECK_PREFIX, 18 | url: `${id}/start`, 19 | json: msg, 20 | }); 21 | if (response.statusCode === 200) { 22 | return true; 23 | } 24 | } 25 | return false; 26 | }; 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:18-alpine 3 | 4 | LABEL maintainer="jakewmeyer@gmail.com" 5 | LABEL autoheal="true" 6 | 7 | HEALTHCHECK --interval=10s --timeout=3s \ 8 | CMD ./lib/utils/healthcheck.js 9 | 10 | RUN apk add --no-cache --upgrade bash 11 | 12 | ENV NODE_ENV=production 13 | ENV HEALTH_URL=http://localhost:6673/v4/admin/health 14 | 15 | EXPOSE 6673 16 | 17 | # Run as an unprivileged user. 18 | RUN addgroup -S spacex && adduser -S -G spacex spacex 19 | RUN mkdir /app && chown spacex /app 20 | USER spacex 21 | 22 | WORKDIR /app 23 | ENTRYPOINT ["/app/start.sh"] 24 | 25 | COPY --chown=spacex:spacex package.json package-lock.json /app/ 26 | 27 | RUN npm install --production 28 | 29 | COPY --chown=spacex:spacex . . 30 | -------------------------------------------------------------------------------- /docs/crew/v4/one.md: -------------------------------------------------------------------------------- 1 | # Get one crew member 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/crew/:id` 6 | 7 | **URL Parameters** : `id=[string]` where `id` is the ID of the crew member 8 | 9 | **Auth required** : `False` 10 | 11 | ## Success Response 12 | 13 | **Code** : `200 OK` 14 | 15 | **Content example** : 16 | 17 | ```json 18 | { 19 | "name": "Douglas Hurley", 20 | "agency": "NASA", 21 | "image": "https://i.imgur.com/ooaayWf.png", 22 | "wikipedia": "https://en.wikipedia.org/wiki/Douglas_G._Hurley", 23 | "launches": [ 24 | "5eb87d46ffd86e000604b388" 25 | ], 26 | "status": "active", 27 | "id": "5ebf1b7323a9a60006e03a7b" 28 | } 29 | ``` 30 | 31 | ## Error Responses 32 | 33 | **Code** : `404 NOT FOUND` 34 | 35 | **Content** : `Not Found` 36 | -------------------------------------------------------------------------------- /docs/history/v4/all.md: -------------------------------------------------------------------------------- 1 | # Get all history events 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/history` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "title": "SpaceX successfully launches humans to ISS", 17 | "event_date_utc": "2020-05-30T19:22:00Z", 18 | "event_date_unix": 1590866520, 19 | "details": "This mission was the first crewed flight to launch from the United States since the end of the Space Shuttle program in 2011. It carried NASA astronauts Doug Hurley and Bob Behnken to the ISS.", 20 | "links": { 21 | "article": "https://spaceflightnow.com/2020/05/30/nasa-astronauts-launch-from-us-soil-for-first-time-in-nine-years/" 22 | } 23 | } 24 | ... 25 | ] 26 | ``` 27 | -------------------------------------------------------------------------------- /models/index.js: -------------------------------------------------------------------------------- 1 | export { default as Capsule } from './capsules.js'; 2 | export { default as Company } from './company.js'; 3 | export { default as Core } from './cores.js'; 4 | export { default as Crew } from './crew.js'; 5 | export { default as Dragon } from './dragons.js'; 6 | export { default as History } from './history.js'; 7 | export { default as Landpad } from './landpads.js'; 8 | export { default as Launch } from './launches.js'; 9 | export { default as Launchpad } from './launchpads.js'; 10 | export { default as Payload } from './payloads.js'; 11 | export { default as Roadster } from './roadster.js'; 12 | export { default as Rocket } from './rockets.js'; 13 | export { default as Ship } from './ships.js'; 14 | export { default as Starlink } from './starlink.js'; 15 | export { default as User } from './users.js'; 16 | -------------------------------------------------------------------------------- /docs/capsules/v4/one.md: -------------------------------------------------------------------------------- 1 | # Get one capsule 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/capsules/:id` 6 | 7 | **URL Parameters** : `id=[string]` where `id` is the ID of the capsule 8 | 9 | **Auth required** : `False` 10 | 11 | ## Success Response 12 | 13 | **Code** : `200 OK` 14 | 15 | **Content example** : 16 | 17 | ```json 18 | { 19 | "reuse_count": 1, 20 | "water_landings": 1, 21 | "land_landings": 0, 22 | "last_update": "Reentered after three weeks in orbit", 23 | "launches": [ 24 | "5eb87cdeffd86e000604b330" 25 | ], 26 | "serial": "C101", 27 | "status": "retired", 28 | "type": "Dragon 1.0", 29 | "id": "5e9e2c5bf35918ed873b2664" 30 | } 31 | ``` 32 | 33 | ## Error Responses 34 | 35 | **Code** : `404 NOT FOUND` 36 | 37 | **Content** : `Not Found` 38 | -------------------------------------------------------------------------------- /docs/cores/v4/all.md: -------------------------------------------------------------------------------- 1 | # Get all cores 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/cores` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "block": 5, 17 | "reuse_count": 3, 18 | "rtls_attempts": 1, 19 | "rtls_landings": 1, 20 | "asds_attempts": 3, 21 | "asds_landings": 3, 22 | "last_update": "Landed on OCISLY as of Jan 29, 2020. ", 23 | "launches": [ 24 | "5eb87d2bffd86e000604b375", 25 | "5eb87d31ffd86e000604b379", 26 | "5eb87d3fffd86e000604b382", 27 | "5eb87d44ffd86e000604b386" 28 | ], 29 | "serial": "B1051", 30 | "status": "active", 31 | "id": "5e9e28a6f35918c0803b265c" 32 | }, 33 | ... 34 | ] 35 | ``` 36 | -------------------------------------------------------------------------------- /routes/company/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { Company } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/company', 7 | }); 8 | 9 | // Get company info 10 | router.get('/', cache(86400), async (ctx) => { 11 | try { 12 | const result = await Company.findOne({}); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Update company info 21 | router.patch('/:id', auth, authz('company:update'), async (ctx) => { 22 | try { 23 | await Company.findByIdAndUpdate(ctx.params.id, ctx.request.body, { runValidators: true }); 24 | ctx.status = 200; 25 | } catch (error) { 26 | ctx.throw(400, error.message); 27 | } 28 | }); 29 | 30 | export default router; 31 | -------------------------------------------------------------------------------- /docs/capsules/v4/schema.md: -------------------------------------------------------------------------------- 1 | # Capsule Schema 2 | 3 | ```json 4 | { 5 | "serial": { 6 | "type": "String", 7 | "required": true, 8 | "unique": true, 9 | }, 10 | "status": { 11 | "type": "String", 12 | "enum": ["unknown", "active", "retired", "destroyed"], 13 | "required": true, 14 | }, 15 | "type": { 16 | "type": "String", 17 | "enum": ["Dragon 1.0", "Dragon 1.1", "Dragon 2.0"], 18 | "required": true, 19 | }, 20 | "dragon": { 21 | "type": "UUID", 22 | }, 23 | "reuse_count": { 24 | "type": "Number", 25 | "default": 0, 26 | }, 27 | "water_landings": { 28 | "type": "Number", 29 | "default": 0, 30 | }, 31 | "land_landings": { 32 | "type": "Number", 33 | "default": 0, 34 | }, 35 | "last_update": { 36 | "type": "String", 37 | "default": null, 38 | }, 39 | "launches": [{ 40 | "type": "UUID", 41 | }], 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /models/history.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import mongoosePaginate from 'mongoose-paginate-v2'; 3 | import idPlugin from 'mongoose-id'; 4 | 5 | const historySchema = new mongoose.Schema({ 6 | title: { 7 | type: String, 8 | default: null, 9 | }, 10 | event_date_utc: { 11 | type: String, 12 | default: null, 13 | }, 14 | event_date_unix: { 15 | type: Number, 16 | default: null, 17 | }, 18 | details: { 19 | type: String, 20 | default: null, 21 | }, 22 | links: { 23 | article: { 24 | type: String, 25 | default: null, 26 | }, 27 | }, 28 | }, { autoCreate: true }); 29 | 30 | const index = { 31 | title: 'text', 32 | details: 'text', 33 | }; 34 | historySchema.index(index); 35 | 36 | historySchema.plugin(mongoosePaginate); 37 | historySchema.plugin(idPlugin); 38 | 39 | const History = mongoose.model('History', historySchema); 40 | 41 | export default History; 42 | -------------------------------------------------------------------------------- /docs/cores/v4/schema.md: -------------------------------------------------------------------------------- 1 | # Core Schema 2 | 3 | ```json 4 | { 5 | "serial": { 6 | "type": "String", 7 | "unique": true, 8 | "required": true, 9 | }, 10 | "block": { 11 | "type": "Number", 12 | "default": null, 13 | }, 14 | "status": { 15 | "type": "String", 16 | "enum": ["active", "inactive", "unknown", "expended", "lost", "retired"], 17 | "required": true, 18 | }, 19 | "reuse_count": { 20 | "type": "Number", 21 | "default": 0, 22 | }, 23 | "rtls_attempts": { 24 | "type": "Number", 25 | "default": 0, 26 | }, 27 | "rtls_landings": { 28 | "type": "Number", 29 | "default": 0, 30 | }, 31 | "asds_attempts": { 32 | "type": "Number", 33 | "default": 0, 34 | }, 35 | "asds_landings": { 36 | "type": "Number", 37 | "default": 0, 38 | }, 39 | "last_update": { 40 | "type": "String", 41 | "default": null, 42 | }, 43 | "launches": [{ 44 | "type": "UUID", 45 | }], 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /models/crew.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import mongoosePaginate from 'mongoose-paginate-v2'; 3 | import idPlugin from 'mongoose-id'; 4 | 5 | const crewSchema = new mongoose.Schema({ 6 | name: { 7 | type: String, 8 | default: null, 9 | }, 10 | status: { 11 | type: String, 12 | required: true, 13 | enum: ['active', 'inactive', 'retired', 'unknown'], 14 | }, 15 | agency: { 16 | type: String, 17 | default: null, 18 | }, 19 | image: { 20 | type: String, 21 | default: null, 22 | }, 23 | wikipedia: { 24 | type: String, 25 | default: null, 26 | }, 27 | launches: [{ 28 | type: mongoose.ObjectId, 29 | ref: 'Launch', 30 | }], 31 | }, { autoCreate: true }); 32 | 33 | const index = { 34 | name: 'text', 35 | }; 36 | crewSchema.index(index); 37 | 38 | crewSchema.plugin(mongoosePaginate); 39 | crewSchema.plugin(idPlugin); 40 | 41 | const Crew = mongoose.model('Crew', crewSchema); 42 | 43 | export default Crew; 44 | -------------------------------------------------------------------------------- /docs/cores/v4/one.md: -------------------------------------------------------------------------------- 1 | # Get one core 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/cores/:id` 6 | 7 | **URL Parameters** : `id=[string]` where `id` is the ID of the core 8 | 9 | **Auth required** : `False` 10 | 11 | ## Success Response 12 | 13 | **Code** : `200 OK` 14 | 15 | **Content example** : 16 | 17 | ```json 18 | { 19 | "block": 5, 20 | "reuse_count": 3, 21 | "rtls_attempts": 1, 22 | "rtls_landings": 1, 23 | "asds_attempts": 3, 24 | "asds_landings": 3, 25 | "last_update": "Landed on OCISLY as of Jan 29, 2020. ", 26 | "launches": [ 27 | "5eb87d2bffd86e000604b375", 28 | "5eb87d31ffd86e000604b379", 29 | "5eb87d3fffd86e000604b382", 30 | "5eb87d44ffd86e000604b386" 31 | ], 32 | "serial": "B1051", 33 | "status": "active", 34 | "id": "5e9e28a6f35918c0803b265c" 35 | } 36 | ``` 37 | 38 | ## Error Responses 39 | 40 | **Code** : `404 NOT FOUND` 41 | 42 | **Content** : `Not Found` 43 | -------------------------------------------------------------------------------- /docs/history/v4/one.md: -------------------------------------------------------------------------------- 1 | # Get one history event 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/history/:id` 6 | 7 | **URL Parameters** : `id=[string]` where `id` is the ID of the history event 8 | 9 | **Auth required** : `False` 10 | 11 | ## Success Response 12 | 13 | **Code** : `200 OK` 14 | 15 | **Content example** : 16 | 17 | ```json 18 | { 19 | "title": "SpaceX successfully launches humans to ISS", 20 | "event_date_utc": "2020-05-30T19:22:00Z", 21 | "event_date_unix": 1590866520, 22 | "details": "This mission was the first crewed flight to launch from the United States since the end of the Space Shuttle program in 2011. It carried NASA astronauts Doug Hurley and Bob Behnken to the ISS.", 23 | "links": { 24 | "article": "https://spaceflightnow.com/2020/05/30/nasa-astronauts-launch-from-us-soil-for-first-time-in-nine-years/" 25 | } 26 | } 27 | ``` 28 | 29 | ## Error Responses 30 | 31 | **Code** : `404 NOT FOUND` 32 | 33 | **Content** : `Not Found` 34 | -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | import Router from 'koa-router'; 3 | 4 | const FOLDERS = await Promise.all([ 5 | import('./admin/index.js'), 6 | import('./capsules/index.js'), 7 | import('./company/index.js'), 8 | import('./cores/index.js'), 9 | import('./crew/index.js'), 10 | import('./dragons/index.js'), 11 | import('./history/index.js'), 12 | import('./landpads/index.js'), 13 | import('./launches/index.js'), 14 | import('./launchpads/index.js'), 15 | import('./payloads/index.js'), 16 | import('./roadster/index.js'), 17 | import('./rockets/index.js'), 18 | import('./ships/index.js'), 19 | import('./starlink/index.js'), 20 | import('./users/index.js'), 21 | ]); 22 | 23 | const ROUTER = new Router(); 24 | 25 | // Register all routes + all versions 26 | export default async () => { 27 | for await (const routeFolder of FOLDERS) { 28 | if (routeFolder?.default) { 29 | for await (const version of routeFolder.default) { 30 | ROUTER.use(version?.default?.routes()); 31 | } 32 | } 33 | } 34 | return ROUTER.routes(); 35 | }; 36 | -------------------------------------------------------------------------------- /docs/launchpads/v4/schema.md: -------------------------------------------------------------------------------- 1 | # Launchpad Schema 2 | 3 | ```json 4 | { 5 | "name": { 6 | "type": "String", 7 | "default": null 8 | }, 9 | "full_name": { 10 | "type": "String", 11 | "default": null 12 | }, 13 | "status": { 14 | "type": "String", 15 | "enum": [ 16 | "active", 17 | "inactive", 18 | "unknown", 19 | "retired", 20 | "lost", 21 | "under construction" 22 | ], 23 | "required": true 24 | }, 25 | "locality": { 26 | "type": "String", 27 | "default": null 28 | }, 29 | "region": { 30 | "type": "String", 31 | "default": null 32 | }, 33 | "timezone": { 34 | "type": "String", 35 | "default": null 36 | }, 37 | "latitude": { 38 | "type": "Number", 39 | "default": null 40 | }, 41 | "longitude": { 42 | "type": "Number", 43 | "default": null 44 | }, 45 | "launch_attempts": { 46 | "type": "Number", 47 | "default": 0 48 | }, 49 | "launch_successes": { 50 | "type": "Number", 51 | "default": 0 52 | }, 53 | "rockets": [ 54 | "UUID" 55 | ], 56 | "launches": [ 57 | "UUID" 58 | ] 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /routes/launches/v4/_transform-query.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | /** 4 | * Transform V4 query into V5 query 5 | * 6 | * @param {Object} ctx Koa context 7 | * @param {function} next Koa next function 8 | * @returns {void} 9 | */ 10 | export default async (query) => { 11 | const transformed = query; 12 | 13 | if (query?.options?.populate) { 14 | if (Array.isArray(query.options.populate)) { 15 | query.options.populate.forEach((item, index) => { 16 | if (_.isObject(item)) { 17 | // Index is not user specified 18 | transformed.options.populate[index].path = transformed.options.populate?.[index]?.path?.replace('crew', 'crew.crew'); 19 | } 20 | }); 21 | } 22 | if (_.isObject(query.options.populate) && !Array.isArray(query.options.populate)) { 23 | transformed.options.populate.path = transformed.options.populate.path?.replace('crew', 'crew.crew'); 24 | } 25 | if (_.isString(query.options.populate)) { 26 | transformed.options.populate = transformed.options.populate?.replace('crew', 'crew.crew'); 27 | } 28 | } 29 | 30 | return transformed; 31 | }; 32 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import mongoose from 'mongoose'; 3 | import logger from './middleware/logger.js'; 4 | import { DEFAULT_PORT } from './lib/constants.js'; 5 | import app from './app.js'; 6 | 7 | const PORT = process.env.PORT ?? DEFAULT_PORT; 8 | const SERVER = http.createServer(app.callback()); 9 | 10 | // Gracefully close Mongo connection 11 | const gracefulShutdown = (msg) => { 12 | logger.info(`Shutdown initiated: ${msg}`); 13 | mongoose.connection.close(false, () => { 14 | logger.info('Mongo closed'); 15 | SERVER.close(() => { 16 | logger.info('Shutting down...'); 17 | process.exit(); 18 | }); 19 | }); 20 | }; 21 | 22 | // Server start 23 | SERVER.listen(PORT, '0.0.0.0', () => { 24 | logger.info(`Running on port: ${PORT}`); 25 | 26 | // Handle kill commands 27 | process.on('SIGTERM', gracefulShutdown); 28 | 29 | // Handle interrupts 30 | process.on('SIGINT', gracefulShutdown); 31 | 32 | // Prevent dirty exit on uncaught exceptions: 33 | process.on('uncaughtException', gracefulShutdown); 34 | 35 | // Prevent dirty exit on unhandled promise rejection 36 | process.on('unhandledRejection', gracefulShutdown); 37 | }); 38 | -------------------------------------------------------------------------------- /docs/company/v4/schema.md: -------------------------------------------------------------------------------- 1 | # Company Info Schema 2 | 3 | ```json 4 | { 5 | "name": { 6 | "type": "String" 7 | }, 8 | "founder": { 9 | "type": "String" 10 | }, 11 | "founded": { 12 | "type": "Number" 13 | }, 14 | "employees": { 15 | "type": "Number" 16 | }, 17 | "vehicles": { 18 | "type": "Number" 19 | }, 20 | "launch_sites": { 21 | "type": "Number" 22 | }, 23 | "test_sites": { 24 | "type": "Number" 25 | }, 26 | "ceo": { 27 | "type": "String" 28 | }, 29 | "cto": { 30 | "type": "String" 31 | }, 32 | "coo": { 33 | "type": "String" 34 | }, 35 | "cto_propulsion": { 36 | "type": "String" 37 | }, 38 | "valuation": { 39 | "type": "Number" 40 | }, 41 | "headquarters": { 42 | "address": { 43 | "type": "String" 44 | }, 45 | "city": { 46 | "type": "String" 47 | }, 48 | "state": { 49 | "type": "String" 50 | } 51 | }, 52 | "links": { 53 | "website": { 54 | "type": "String" 55 | }, 56 | "flickr": { 57 | "type": "String" 58 | }, 59 | "twitter": { 60 | "type": "String" 61 | }, 62 | "elon_twitter": { 63 | "type": "String" 64 | } 65 | }, 66 | "summary": { 67 | "type": "String" 68 | } 69 | } 70 | ``` 71 | -------------------------------------------------------------------------------- /routes/roadster/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { Roadster } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/roadster', 7 | }); 8 | 9 | // Get roadster 10 | router.get('/', cache(300), async (ctx) => { 11 | try { 12 | const result = await Roadster.findOne({}); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Query roadster 21 | router.post('/query', cache(300), async (ctx) => { 22 | const { query = {}, options = { select: '' } } = ctx.request.body; 23 | try { 24 | const result = await Roadster.findOne(query).select(options.select).exec(); 25 | ctx.status = 200; 26 | ctx.body = result; 27 | } catch (error) { 28 | ctx.throw(400, error.message); 29 | } 30 | }); 31 | 32 | // Update roadster 33 | router.patch('/:id', auth, authz('roadster:update'), async (ctx) => { 34 | try { 35 | await Roadster.findByIdAndUpdate(ctx.params.id, ctx.request.body, { runValidators: true }); 36 | ctx.status = 200; 37 | } catch (error) { 38 | ctx.throw(400, error.message); 39 | } 40 | }); 41 | 42 | export default router; 43 | -------------------------------------------------------------------------------- /docs/company/v4/all.md: -------------------------------------------------------------------------------- 1 | # Get all company info 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/company` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | { 15 | "headquarters": { 16 | "address": "Rocket Road", 17 | "city": "Hawthorne", 18 | "state": "California" 19 | }, 20 | "links": { 21 | "website": "https://www.spacex.com/", 22 | "flickr": "https://www.flickr.com/photos/spacex/", 23 | "twitter": "https://twitter.com/SpaceX", 24 | "elon_twitter": "https://twitter.com/elonmusk" 25 | }, 26 | "name": "SpaceX", 27 | "founder": "Elon Musk", 28 | "founded": 2002, 29 | "employees": 8000, 30 | "vehicles": 3, 31 | "launch_sites": 3, 32 | "test_sites": 1, 33 | "ceo": "Elon Musk", 34 | "cto": "Elon Musk", 35 | "coo": "Gwynne Shotwell", 36 | "cto_propulsion": "Tom Mueller", 37 | "valuation": 52000000000, 38 | "summary": "SpaceX designs, manufactures and launches advanced rockets and spacecraft. The company was founded in 2002 to revolutionize space technology, with the ultimate goal of enabling people to live on other planets.", 39 | "id": "5eb75edc42fea42237d7f3ed" 40 | } 41 | ``` 42 | -------------------------------------------------------------------------------- /routes/launches/v4/_transform-response.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | const buildCrew = (launch) => { 3 | if (Array.isArray(launch?.crew) && launch.crew.length === 0) { 4 | return []; 5 | } 6 | return launch.crew.map((crew) => { 7 | if (crew?.crew) { 8 | return crew?.crew; 9 | } 10 | return crew; 11 | }); 12 | }; 13 | 14 | export default async (payload) => { 15 | if (Array.isArray(payload)) { 16 | return payload.map((launch) => { 17 | if (Array.isArray(launch?.crew)) { 18 | return { 19 | ...launch.toObject(), 20 | crew: buildCrew(launch.toObject()), 21 | }; 22 | } 23 | return { 24 | ...launch.toObject(), 25 | }; 26 | }); 27 | } 28 | if (Array.isArray(payload?.docs)) { 29 | const docs = payload.docs.map((launch) => { 30 | if (Array.isArray(launch?.crew)) { 31 | return { 32 | ...launch.toObject(), 33 | crew: buildCrew(launch.toObject()), 34 | }; 35 | } 36 | return { 37 | ...launch.toObject(), 38 | }; 39 | }); 40 | return { 41 | ...payload, 42 | docs, 43 | }; 44 | } 45 | return { 46 | ...payload.toObject(), 47 | crew: buildCrew(payload.toObject()), 48 | }; 49 | }; 50 | -------------------------------------------------------------------------------- /docs/landpads/v4/schema.md: -------------------------------------------------------------------------------- 1 | # Landing Pad Schema 2 | 3 | ```json 4 | { 5 | "name": { 6 | "type": "String", 7 | "default": null 8 | }, 9 | "full_name": { 10 | "type": "String", 11 | "default": null 12 | }, 13 | "status": { 14 | "type": "String", 15 | "enum": [ 16 | "active", 17 | "inactive", 18 | "unknown", 19 | "retired", 20 | "lost", 21 | "under construction" 22 | ], 23 | "required": true 24 | }, 25 | "type": { 26 | "type": "String", 27 | "default": null 28 | }, 29 | "locality": { 30 | "type": "String", 31 | "default": null 32 | }, 33 | "region": { 34 | "type": "String", 35 | "default": null 36 | }, 37 | "latitude": { 38 | "type": "Number", 39 | "default": null 40 | }, 41 | "longitude": { 42 | "type": "Number", 43 | "default": null 44 | }, 45 | "landing_attempts": { 46 | "type": "Number", 47 | "default": 0 48 | }, 49 | "landing_successes": { 50 | "type": "Number", 51 | "default": 0 52 | }, 53 | "wikipedia": { 54 | "type": "String", 55 | "default": null 56 | }, 57 | "details": { 58 | "type": "String", 59 | "default": null 60 | }, 61 | "launches": [ 62 | { 63 | "type": "UUID" 64 | } 65 | ] 66 | } 67 | ``` 68 | -------------------------------------------------------------------------------- /docs/ships/v4/all.md: -------------------------------------------------------------------------------- 1 | # Get all ships 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/ships` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "legacy_id": "GOPURSUIT", 17 | "model": null, 18 | "type": "Cargo", 19 | "roles": [ 20 | "Support Ship", 21 | "Fairing Recovery" 22 | ], 23 | "imo": 9458884, 24 | "mmsi": 367191410, 25 | "abs": 1201189, 26 | "class": 7174230, 27 | "mass_kg": 502999, 28 | "mass_lbs": 1108925, 29 | "year_built": 2007, 30 | "home_port": "Port Canaveral", 31 | "status": "", 32 | "speed_kn": null, 33 | "course_deg": null, 34 | "latitude": null, 35 | "longitude": null, 36 | "last_ais_update": null, 37 | "link": "https://www.marinetraffic.com/en/ais/details/ships/shipid:439594/mmsi:367191410/imo:9458884/vessel:GO_PURSUIT", 38 | "image": "https://i.imgur.com/5w1ZWre.jpg", 39 | "launches": [ 40 | "5eb87d18ffd86e000604b365", 41 | "5eb87d19ffd86e000604b366", 42 | "5eb87d1bffd86e000604b368", 43 | "5eb87d1effd86e000604b36a" 44 | ], 45 | "name": "GO Pursuit", 46 | "active": false, 47 | "id": "5ea6ed2e080df4000697c90a" 48 | }, 49 | ... 50 | ] 51 | ``` 52 | -------------------------------------------------------------------------------- /docs/landpads/v4/all.md: -------------------------------------------------------------------------------- 1 | # Get all landing pads 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/landpads` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "name": "LZ-2", 17 | "full_name": "Landing Zone 2", 18 | "status": "active", 19 | "type": "RTLS", 20 | "locality": "Cape Canaveral", 21 | "region": "Florida", 22 | "latitude": 28.485833, 23 | "longitude": -80.544444, 24 | "landing_attempts": 3, 25 | "landing_successes": 3, 26 | "wikipedia": "https://en.wikipedia.org/wiki/Landing_Zones_1_and_2", 27 | "details": "SpaceX's first east coast landing pad is Landing Zone 1, where the historic first Falcon 9 landing occurred in December 2015. LC-13 was originally used as a launch pad for early Atlas missiles and rockets from Lockheed Martin. LC-1 was later expanded to include Landing Zone 2 for side booster RTLS Falcon Heavy missions, and it was first used in February 2018 for that purpose.", 28 | "launches": [ 29 | "5eb87d13ffd86e000604b360", 30 | "5eb87d2dffd86e000604b376", 31 | "5eb87d35ffd86e000604b37a" 32 | ], 33 | "id": "5e9e3032383ecb90a834e7c8" 34 | }, 35 | ... 36 | ] 37 | ``` 38 | -------------------------------------------------------------------------------- /models/capsules.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import mongoosePaginate from 'mongoose-paginate-v2'; 3 | import idPlugin from 'mongoose-id'; 4 | 5 | const capsuleSchema = new mongoose.Schema({ 6 | serial: { 7 | type: String, 8 | required: true, 9 | unique: true, 10 | }, 11 | status: { 12 | type: String, 13 | enum: ['unknown', 'active', 'retired', 'destroyed'], 14 | required: true, 15 | }, 16 | type: { 17 | type: String, 18 | enum: ['Dragon 1.0', 'Dragon 1.1', 'Dragon 2.0'], 19 | required: true, 20 | }, 21 | dragon: { 22 | type: mongoose.ObjectId, 23 | ref: 'Dragon', 24 | }, 25 | reuse_count: { 26 | type: Number, 27 | default: 0, 28 | }, 29 | water_landings: { 30 | type: Number, 31 | default: 0, 32 | }, 33 | land_landings: { 34 | type: Number, 35 | default: 0, 36 | }, 37 | last_update: { 38 | type: String, 39 | default: null, 40 | }, 41 | launches: [{ 42 | type: mongoose.ObjectId, 43 | ref: 'Launch', 44 | }], 45 | }, { autoCreate: true }); 46 | 47 | const index = { 48 | serial: 'text', 49 | last_update: 'text', 50 | }; 51 | capsuleSchema.index(index); 52 | 53 | capsuleSchema.plugin(mongoosePaginate); 54 | capsuleSchema.plugin(idPlugin); 55 | 56 | const Capsule = mongoose.model('Capsule', capsuleSchema); 57 | 58 | export default Capsule; 59 | -------------------------------------------------------------------------------- /models/cores.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import mongoosePaginate from 'mongoose-paginate-v2'; 3 | import idPlugin from 'mongoose-id'; 4 | 5 | const coreSchema = new mongoose.Schema({ 6 | serial: { 7 | type: String, 8 | unique: true, 9 | required: true, 10 | }, 11 | block: { 12 | type: Number, 13 | default: null, 14 | }, 15 | status: { 16 | type: String, 17 | enum: ['active', 'inactive', 'unknown', 'expended', 'lost', 'retired'], 18 | required: true, 19 | }, 20 | reuse_count: { 21 | type: Number, 22 | default: 0, 23 | }, 24 | rtls_attempts: { 25 | type: Number, 26 | default: 0, 27 | }, 28 | rtls_landings: { 29 | type: Number, 30 | default: 0, 31 | }, 32 | asds_attempts: { 33 | type: Number, 34 | default: 0, 35 | }, 36 | asds_landings: { 37 | type: Number, 38 | default: 0, 39 | }, 40 | last_update: { 41 | type: String, 42 | default: null, 43 | }, 44 | launches: [{ 45 | type: mongoose.ObjectId, 46 | ref: 'Launch', 47 | }], 48 | }, { autoCreate: true }); 49 | 50 | const index = { 51 | serial: 'text', 52 | last_update: 'text', 53 | }; 54 | coreSchema.index(index); 55 | 56 | coreSchema.plugin(mongoosePaginate); 57 | coreSchema.plugin(idPlugin); 58 | 59 | const Core = mongoose.model('Core', coreSchema); 60 | 61 | export default Core; 62 | -------------------------------------------------------------------------------- /docs/crew/v4/query.md: -------------------------------------------------------------------------------- 1 | # Query crew members 2 | 3 | **Method** : `POST` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/crew/query` 6 | 7 | **Auth required** : `False` 8 | 9 | **Body** : 10 | 11 | See [query](../../queries.md) guide for more details on building queries and paginating results. 12 | 13 | ```json 14 | { 15 | "query": {}, 16 | "options": {} 17 | } 18 | ``` 19 | 20 | ## Success Response 21 | 22 | **Code** : `200 OK` 23 | 24 | **Content example** : 25 | 26 | ```json 27 | { 28 | "docs": [ 29 | { 30 | "name": "Robert Behnken", 31 | "agency": "NASA", 32 | "image": "https://imgur.com/0smMgMH.png", 33 | "wikipedia": "https://en.wikipedia.org/wiki/Robert_L._Behnken", 34 | "launches": [ 35 | "5eb87d46ffd86e000604b388" 36 | ], 37 | "status": "active", 38 | "id": "5ebf1a6e23a9a60006e03a7a" 39 | }, 40 | ... 41 | ], 42 | "totalDocs": 2, 43 | "offset": 0, 44 | "limit": 10, 45 | "totalPages": 1, 46 | "page": 1, 47 | "pagingCounter": 1, 48 | "hasPrevPage": false, 49 | "hasNextPage": false, 50 | "prevPage": null, 51 | "nextPage": null 52 | } 53 | ``` 54 | 55 | ## Error Responses 56 | 57 | **Code** : `400 Bad Request` 58 | 59 | **Content** : Mongoose error is shown, with suggestions to fix the query. 60 | -------------------------------------------------------------------------------- /models/company.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import idPlugin from 'mongoose-id'; 3 | 4 | const companySchema = new mongoose.Schema({ 5 | name: { 6 | type: String, 7 | }, 8 | founder: { 9 | type: String, 10 | }, 11 | founded: { 12 | type: Number, 13 | }, 14 | employees: { 15 | type: Number, 16 | }, 17 | vehicles: { 18 | type: Number, 19 | }, 20 | launch_sites: { 21 | type: Number, 22 | }, 23 | test_sites: { 24 | type: Number, 25 | }, 26 | ceo: { 27 | type: String, 28 | }, 29 | cto: { 30 | type: String, 31 | }, 32 | coo: { 33 | type: String, 34 | }, 35 | cto_propulsion: { 36 | type: String, 37 | }, 38 | valuation: { 39 | type: Number, 40 | }, 41 | headquarters: { 42 | address: { 43 | type: String, 44 | }, 45 | city: { 46 | type: String, 47 | }, 48 | state: { 49 | type: String, 50 | }, 51 | }, 52 | links: { 53 | website: { 54 | type: String, 55 | }, 56 | flickr: { 57 | type: String, 58 | }, 59 | twitter: { 60 | type: String, 61 | }, 62 | elon_twitter: { 63 | type: String, 64 | }, 65 | }, 66 | summary: { 67 | type: String, 68 | }, 69 | }, { autoCreate: true }); 70 | 71 | companySchema.plugin(idPlugin); 72 | 73 | const Company = mongoose.model('Company', companySchema); 74 | 75 | export default Company; 76 | -------------------------------------------------------------------------------- /docs/capsules/v4/query.md: -------------------------------------------------------------------------------- 1 | # Query capsules 2 | 3 | **Method** : `POST` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/capsules/query` 6 | 7 | **Auth required** : `False` 8 | 9 | **Body** : 10 | 11 | See [query](../../queries.md) guide for more details on building queries and paginating results. 12 | 13 | ```json 14 | { 15 | "query": {}, 16 | "options": {} 17 | } 18 | ``` 19 | 20 | ## Success Response 21 | 22 | **Code** : `200 OK` 23 | 24 | **Content example** : 25 | 26 | ```json 27 | { 28 | "docs": [ 29 | { 30 | "reuse_count": 1, 31 | "water_landings": 1, 32 | "land_landings": 0, 33 | "last_update": "Reentered after three weeks in orbit", 34 | "launches": [ 35 | "5eb87cdeffd86e000604b330" 36 | ], 37 | "serial": "C101", 38 | "status": "retired", 39 | "type": "Dragon 1.0", 40 | "id": "5e9e2c5bf35918ed873b2664" 41 | }, 42 | ... 43 | ], 44 | "totalDocs": 19, 45 | "offset": 0, 46 | "limit": 10, 47 | "totalPages": 2, 48 | "page": 1, 49 | "pagingCounter": 1, 50 | "hasPrevPage": false, 51 | "hasNextPage": true, 52 | "prevPage": null, 53 | "nextPage": 2 54 | } 55 | ``` 56 | 57 | ## Error Responses 58 | 59 | **Code** : `400 Bad Request` 60 | 61 | **Content** : Mongoose error is shown, with suggestions to fix the query. 62 | -------------------------------------------------------------------------------- /docs/landpads/v4/one.md: -------------------------------------------------------------------------------- 1 | # Get one landing pad 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/landpads/:id` 6 | 7 | **URL Parameters** : `id=[string]` where `id` is the ID of the landing pad 8 | 9 | **Auth required** : `False` 10 | 11 | ## Success Response 12 | 13 | **Code** : `200 OK` 14 | 15 | **Content example** : 16 | 17 | ```json 18 | { 19 | "name": "LZ-2", 20 | "full_name": "Landing Zone 2", 21 | "status": "active", 22 | "type": "RTLS", 23 | "locality": "Cape Canaveral", 24 | "region": "Florida", 25 | "latitude": 28.485833, 26 | "longitude": -80.544444, 27 | "landing_attempts": 3, 28 | "landing_successes": 3, 29 | "wikipedia": "https://en.wikipedia.org/wiki/Landing_Zones_1_and_2", 30 | "details": "SpaceX's first east coast landing pad is Landing Zone 1, where the historic first Falcon 9 landing occurred in December 2015. LC-13 was originally used as a launch pad for early Atlas missiles and rockets from Lockheed Martin. LC-1 was later expanded to include Landing Zone 2 for side booster RTLS Falcon Heavy missions, and it was first used in February 2018 for that purpose.", 31 | "launches": [ 32 | "5eb87d13ffd86e000604b360", 33 | "5eb87d2dffd86e000604b376", 34 | "5eb87d35ffd86e000604b37a" 35 | ], 36 | "id": "5e9e3032383ecb90a834e7c8" 37 | } 38 | ``` 39 | 40 | ## Error Responses 41 | 42 | **Code** : `404 NOT FOUND` 43 | 44 | **Content** : `Not Found` 45 | -------------------------------------------------------------------------------- /docs/ships/v4/one.md: -------------------------------------------------------------------------------- 1 | # Get one ship 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/ships/:id` 6 | 7 | **URL Parameters** : `id=[string]` where `id` is the ID of the ship 8 | 9 | **Auth required** : `False` 10 | 11 | ## Success Response 12 | 13 | **Code** : `200 OK` 14 | 15 | **Content example** : 16 | 17 | ```json 18 | { 19 | "legacy_id": "GOPURSUIT", 20 | "model": null, 21 | "type": "Cargo", 22 | "roles": [ 23 | "Support Ship", 24 | "Fairing Recovery" 25 | ], 26 | "imo": 9458884, 27 | "mmsi": 367191410, 28 | "abs": 1201189, 29 | "class": 7174230, 30 | "mass_kg": 502999, 31 | "mass_lbs": 1108925, 32 | "year_built": 2007, 33 | "home_port": "Port Canaveral", 34 | "status": "", 35 | "speed_kn": null, 36 | "course_deg": null, 37 | "latitude": null, 38 | "longitude": null, 39 | "last_ais_update": null, 40 | "link": "https://www.marinetraffic.com/en/ais/details/ships/shipid:439594/mmsi:367191410/imo:9458884/vessel:GO_PURSUIT", 41 | "image": "https://i.imgur.com/5w1ZWre.jpg", 42 | "launches": [ 43 | "5eb87d18ffd86e000604b365", 44 | "5eb87d19ffd86e000604b366", 45 | "5eb87d1bffd86e000604b368", 46 | "5eb87d1effd86e000604b36a" 47 | ], 48 | "name": "GO Pursuit", 49 | "active": false, 50 | "id": "5ea6ed2e080df4000697c90a" 51 | } 52 | ``` 53 | 54 | ## Error Responses 55 | 56 | **Code** : `404 NOT FOUND` 57 | 58 | **Content** : `Not Found` 59 | -------------------------------------------------------------------------------- /docs/launchpads/v4/all.md: -------------------------------------------------------------------------------- 1 | # Get all launchpads 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/launchpads` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "name": "VAFB SLC 4E", 17 | "full_name": "Vandenberg Air Force Base Space Launch Complex 4E", 18 | "locality": "Vandenberg Air Force Base", 19 | "region": "California", 20 | "timezone": "America/Los_Angeles", 21 | "latitude": 34.632093, 22 | "longitude": -120.610829, 23 | "launch_attempts": 15, 24 | "launch_successes": 15, 25 | "rockets": [ 26 | "5e9d0d95eda69973a809d1ec" 27 | ], 28 | "launches": [ 29 | "5eb87ce1ffd86e000604b334", 30 | "5eb87cf0ffd86e000604b343", 31 | "5eb87cfdffd86e000604b34c", 32 | "5eb87d05ffd86e000604b354", 33 | "5eb87d08ffd86e000604b357", 34 | "5eb87d0affd86e000604b359", 35 | "5eb87d0fffd86e000604b35d", 36 | "5eb87d14ffd86e000604b361", 37 | "5eb87d16ffd86e000604b363", 38 | "5eb87d1affd86e000604b367", 39 | "5eb87d1fffd86e000604b36b", 40 | "5eb87d23ffd86e000604b36e", 41 | "5eb87d25ffd86e000604b370", 42 | "5eb87d28ffd86e000604b373", 43 | "5eb87d31ffd86e000604b379" 44 | ], 45 | "status": "active", 46 | "id": "5e9e4502f509092b78566f87" 47 | }, 48 | ... 49 | ] 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/history/v4/query.md: -------------------------------------------------------------------------------- 1 | # Query history events 2 | 3 | **Method** : `POST` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/history/query` 6 | 7 | **Auth required** : `False` 8 | 9 | **Body** : 10 | 11 | See [query](../../queries.md) guide for more details on building queries and paginating results. 12 | 13 | ```json 14 | { 15 | "query": {}, 16 | "options": {} 17 | } 18 | ``` 19 | 20 | ## Success Response 21 | 22 | **Code** : `200 OK` 23 | 24 | **Content example** : 25 | 26 | ```json 27 | { 28 | "docs": [ 29 | { 30 | "title": "SpaceX successfully launches humans to ISS", 31 | "event_date_utc": "2020-05-30T19:22:00Z", 32 | "event_date_unix": 1590866520, 33 | "details": "This mission was the first crewed flight to launch from the United States since the end of the Space Shuttle program in 2011. It carried NASA astronauts Doug Hurley and Bob Behnken to the ISS.", 34 | "links": { 35 | "article": "https://spaceflightnow.com/2020/05/30/nasa-astronauts-launch-from-us-soil-for-first-time-in-nine-years/" 36 | } 37 | } 38 | ... 39 | ], 40 | "totalDocs": 7, 41 | "offset": 0, 42 | "limit": 10, 43 | "totalPages": 1, 44 | "page": 1, 45 | "pagingCounter": 1, 46 | "hasPrevPage": false, 47 | "hasNextPage": false, 48 | "prevPage": null, 49 | "nextPage": null 50 | } 51 | ``` 52 | 53 | ## Error Responses 54 | 55 | **Code** : `400 Bad Request` 56 | 57 | **Content** : Mongoose error is shown, with suggestions to fix the query. 58 | -------------------------------------------------------------------------------- /docs/cores/v4/query.md: -------------------------------------------------------------------------------- 1 | # Query cores 2 | 3 | **Method** : `POST` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/cores/query` 6 | 7 | **Auth required** : `False` 8 | 9 | **Body** : 10 | 11 | See [query](../../queries.md) guide for more details on building queries and paginating results. 12 | 13 | ```json 14 | { 15 | "query": {}, 16 | "options": {} 17 | } 18 | ``` 19 | 20 | ## Success Response 21 | 22 | **Code** : `200 OK` 23 | 24 | **Content example** : 25 | 26 | ```json 27 | { 28 | "docs": [ 29 | { 30 | "block": 5, 31 | "reuse_count": 3, 32 | "rtls_attempts": 1, 33 | "rtls_landings": 1, 34 | "asds_attempts": 2, 35 | "asds_landings": 2, 36 | "last_update": "Missed the droneship and made successful water landing; apparently scuttled at sea afterward. ", 37 | "launches": [ 38 | "5eb87d2effd86e000604b377", 39 | "5eb87d36ffd86e000604b37b", 40 | "5eb87d3bffd86e000604b37f", 41 | "5eb87d41ffd86e000604b383" 42 | ], 43 | "serial": "B1056", 44 | "status": "lost", 45 | "id": "5e9e28a7f3591809313b2660" 46 | }, 47 | ... 48 | ], 49 | "totalDocs": 65, 50 | "offset": 0, 51 | "limit": 10, 52 | "totalPages": 7, 53 | "page": 1, 54 | "pagingCounter": 1, 55 | "hasPrevPage": false, 56 | "hasNextPage": true, 57 | "prevPage": null, 58 | "nextPage": 2 59 | } 60 | ``` 61 | 62 | ## Error Responses 63 | 64 | **Code** : `400 Bad Request` 65 | 66 | **Content** : Mongoose error is shown, with suggestions to fix the query. 67 | -------------------------------------------------------------------------------- /docs/roadster/v4/schema.md: -------------------------------------------------------------------------------- 1 | # Roadster Schema 2 | 3 | ```json 4 | { 5 | "name": { 6 | "type": "String" 7 | }, 8 | "launch_date_utc": { 9 | "type": "String" 10 | }, 11 | "launch_date_unix": { 12 | "type": "Number" 13 | }, 14 | "launch_mass_kg": { 15 | "type": "Number" 16 | }, 17 | "launch_mass_lbs": { 18 | "type": "Number" 19 | }, 20 | "norad_id": { 21 | "type": "Number" 22 | }, 23 | "epoch_jd": { 24 | "type": "Number" 25 | }, 26 | "orbit_type": { 27 | "type": "String" 28 | }, 29 | "apoapsis_au": { 30 | "type": "Number" 31 | }, 32 | "periapsis_au": { 33 | "type": "Number" 34 | }, 35 | "semi_major_axis_au": { 36 | "type": "Number" 37 | }, 38 | "eccentricity": { 39 | "type": "Number" 40 | }, 41 | "inclination": { 42 | "type": "Number" 43 | }, 44 | "longitude": { 45 | "type": "Number" 46 | }, 47 | "periapsis_arg": { 48 | "type": "Number" 49 | }, 50 | "period_days": { 51 | "type": "Number" 52 | }, 53 | "speed_kph": { 54 | "type": "Number" 55 | }, 56 | "speed_mph": { 57 | "type": "Number" 58 | }, 59 | "earth_distance_km": { 60 | "type": "Number" 61 | }, 62 | "earth_distance_mi": { 63 | "type": "Number" 64 | }, 65 | "mars_distance_km": { 66 | "type": "Number" 67 | }, 68 | "mars_distance_mi": { 69 | "type": "Number" 70 | }, 71 | "flickr_images": [ 72 | "String" 73 | ], 74 | "wikipedia": { 75 | "type": "String" 76 | }, 77 | "video": { 78 | "type": "String" 79 | }, 80 | "details": { 81 | "type": "String" 82 | } 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/launchpads/v4/one.md: -------------------------------------------------------------------------------- 1 | # Get one launchpad 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/launchpads/:id` 6 | 7 | **URL Parameters** : `id=[string]` where `id` is the ID of the launchpad 8 | 9 | **Auth required** : `False` 10 | 11 | ## Success Response 12 | 13 | **Code** : `200 OK` 14 | 15 | **Content example** : 16 | 17 | ```json 18 | { 19 | "name": "VAFB SLC 4E", 20 | "full_name": "Vandenberg Air Force Base Space Launch Complex 4E", 21 | "locality": "Vandenberg Air Force Base", 22 | "region": "California", 23 | "timezone": "America/Los_Angeles", 24 | "latitude": 34.632093, 25 | "longitude": -120.610829, 26 | "launch_attempts": 15, 27 | "launch_successes": 15, 28 | "rockets": [ 29 | "5e9d0d95eda69973a809d1ec" 30 | ], 31 | "launches": [ 32 | "5eb87ce1ffd86e000604b334", 33 | "5eb87cf0ffd86e000604b343", 34 | "5eb87cfdffd86e000604b34c", 35 | "5eb87d05ffd86e000604b354", 36 | "5eb87d08ffd86e000604b357", 37 | "5eb87d0affd86e000604b359", 38 | "5eb87d0fffd86e000604b35d", 39 | "5eb87d14ffd86e000604b361", 40 | "5eb87d16ffd86e000604b363", 41 | "5eb87d1affd86e000604b367", 42 | "5eb87d1fffd86e000604b36b", 43 | "5eb87d23ffd86e000604b36e", 44 | "5eb87d25ffd86e000604b370", 45 | "5eb87d28ffd86e000604b373", 46 | "5eb87d31ffd86e000604b379" 47 | ], 48 | "status": "active", 49 | "id": "5e9e4502f509092b78566f87" 50 | } 51 | ``` 52 | 53 | ## Error Responses 54 | 55 | **Code** : `404 NOT FOUND` 56 | 57 | **Content** : `Not Found` 58 | -------------------------------------------------------------------------------- /models/landpads.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import mongoosePaginate from 'mongoose-paginate-v2'; 3 | import idPlugin from 'mongoose-id'; 4 | 5 | const landpadSchema = new mongoose.Schema({ 6 | name: { 7 | type: String, 8 | default: null, 9 | }, 10 | full_name: { 11 | type: String, 12 | default: null, 13 | }, 14 | status: { 15 | type: String, 16 | enum: ['active', 'inactive', 'unknown', 'retired', 'lost', 'under construction'], 17 | required: true, 18 | }, 19 | type: { 20 | type: String, 21 | default: null, 22 | }, 23 | locality: { 24 | type: String, 25 | default: null, 26 | }, 27 | region: { 28 | type: String, 29 | default: null, 30 | }, 31 | latitude: { 32 | type: Number, 33 | default: null, 34 | }, 35 | longitude: { 36 | type: Number, 37 | default: null, 38 | }, 39 | landing_attempts: { 40 | type: Number, 41 | default: 0, 42 | }, 43 | landing_successes: { 44 | type: Number, 45 | default: 0, 46 | }, 47 | wikipedia: { 48 | type: String, 49 | default: null, 50 | }, 51 | details: { 52 | type: String, 53 | default: null, 54 | }, 55 | launches: [{ 56 | type: mongoose.ObjectId, 57 | ref: 'Launch', 58 | }], 59 | images: { 60 | large: [String], 61 | }, 62 | }, { autoCreate: true }); 63 | 64 | const index = { 65 | name: 'text', 66 | full_name: 'text', 67 | details: 'text', 68 | }; 69 | landpadSchema.index(index); 70 | 71 | landpadSchema.plugin(mongoosePaginate); 72 | landpadSchema.plugin(idPlugin); 73 | 74 | const Landpad = mongoose.model('Landpad', landpadSchema); 75 | 76 | export default Landpad; 77 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import conditional from 'koa-conditional-get'; 2 | import etag from 'koa-etag'; 3 | import cors from 'koa2-cors'; 4 | import dotenv from 'dotenv'; 5 | import helmet from 'koa-helmet'; 6 | import Koa from 'koa'; 7 | import bodyParser from 'koa-bodyparser'; 8 | import mongoose from 'mongoose'; 9 | import { responseTime, errors, logger } from './middleware/index.js'; 10 | import routes from './routes/index.js'; 11 | 12 | // Env init 13 | dotenv.config(); 14 | 15 | const app = new Koa(); 16 | 17 | mongoose.connect(process.env.SPACEX_MONGO, { 18 | bufferCommands: false, 19 | family: 4, 20 | }); 21 | 22 | const db = mongoose.connection; 23 | 24 | db.on('error', (err) => { 25 | logger.error(err); 26 | }); 27 | db.once('connected', () => { 28 | logger.info('Mongo connected'); 29 | app.emit('ready'); 30 | }); 31 | db.on('reconnected', () => { 32 | logger.info('Mongo re-connected'); 33 | }); 34 | db.on('disconnected', () => { 35 | logger.info('Mongo disconnected'); 36 | }); 37 | 38 | // disable console.errors for pino 39 | app.silent = true; 40 | 41 | // Error handler 42 | app.use(errors); 43 | 44 | app.use(conditional()); 45 | 46 | app.use(etag()); 47 | 48 | app.use(bodyParser()); 49 | 50 | // HTTP header security 51 | app.use(helmet()); 52 | 53 | // Enable CORS for all routes 54 | app.use(cors({ 55 | origin: '*', 56 | allowMethods: ['GET', 'POST', 'PATCH', 'DELETE'], 57 | allowHeaders: ['Content-Type', 'Accept'], 58 | exposeHeaders: ['spacex-api-cache', 'spacex-api-response-time'], 59 | })); 60 | 61 | // Set header with API response time 62 | app.use(responseTime); 63 | 64 | // Register routes 65 | app.use(await routes()); 66 | 67 | export default app; 68 | -------------------------------------------------------------------------------- /models/launchpads.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import mongoosePaginate from 'mongoose-paginate-v2'; 3 | import idPlugin from 'mongoose-id'; 4 | 5 | const launchpadSchema = new mongoose.Schema({ 6 | name: { 7 | type: String, 8 | default: null, 9 | }, 10 | full_name: { 11 | type: String, 12 | default: null, 13 | }, 14 | status: { 15 | type: String, 16 | enum: ['active', 'inactive', 'unknown', 'retired', 'lost', 'under construction'], 17 | required: true, 18 | }, 19 | locality: { 20 | type: String, 21 | default: null, 22 | }, 23 | region: { 24 | type: String, 25 | default: null, 26 | }, 27 | timezone: { 28 | type: String, 29 | default: null, 30 | }, 31 | latitude: { 32 | type: Number, 33 | default: null, 34 | }, 35 | longitude: { 36 | type: Number, 37 | default: null, 38 | }, 39 | launch_attempts: { 40 | type: Number, 41 | default: 0, 42 | }, 43 | launch_successes: { 44 | type: Number, 45 | default: 0, 46 | }, 47 | rockets: [{ 48 | type: mongoose.ObjectId, 49 | ref: 'Rocket', 50 | }], 51 | launches: [{ 52 | type: mongoose.ObjectId, 53 | ref: 'Launch', 54 | }], 55 | details: { 56 | type: String, 57 | default: null, 58 | }, 59 | images: { 60 | large: [String], 61 | }, 62 | }, { autoCreate: true }); 63 | 64 | const index = { 65 | name: 'text', 66 | full_name: 'text', 67 | details: 'text', 68 | }; 69 | launchpadSchema.index(index); 70 | 71 | launchpadSchema.plugin(mongoosePaginate); 72 | launchpadSchema.plugin(idPlugin); 73 | 74 | const Launchpad = mongoose.model('Launchpad', launchpadSchema); 75 | 76 | export default Launchpad; 77 | -------------------------------------------------------------------------------- /models/roadster.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import idPlugin from 'mongoose-id'; 3 | 4 | const roadsterSchema = new mongoose.Schema({ 5 | name: { 6 | type: String, 7 | }, 8 | launch_date_utc: { 9 | type: String, 10 | }, 11 | launch_date_unix: { 12 | type: Number, 13 | }, 14 | launch_mass_kg: { 15 | type: Number, 16 | }, 17 | launch_mass_lbs: { 18 | type: Number, 19 | }, 20 | norad_id: { 21 | type: Number, 22 | }, 23 | epoch_jd: { 24 | type: Number, 25 | }, 26 | orbit_type: { 27 | type: String, 28 | }, 29 | apoapsis_au: { 30 | type: Number, 31 | }, 32 | periapsis_au: { 33 | type: Number, 34 | }, 35 | semi_major_axis_au: { 36 | type: Number, 37 | }, 38 | eccentricity: { 39 | type: Number, 40 | }, 41 | inclination: { 42 | type: Number, 43 | }, 44 | longitude: { 45 | type: Number, 46 | }, 47 | periapsis_arg: { 48 | type: Number, 49 | }, 50 | period_days: { 51 | type: Number, 52 | }, 53 | speed_kph: { 54 | type: Number, 55 | }, 56 | speed_mph: { 57 | type: Number, 58 | }, 59 | earth_distance_km: { 60 | type: Number, 61 | }, 62 | earth_distance_mi: { 63 | type: Number, 64 | }, 65 | mars_distance_km: { 66 | type: Number, 67 | }, 68 | mars_distance_mi: { 69 | type: Number, 70 | }, 71 | flickr_images: [String], 72 | wikipedia: { 73 | type: String, 74 | }, 75 | video: { 76 | type: String, 77 | }, 78 | details: { 79 | type: String, 80 | }, 81 | }, { autoCreate: true }); 82 | 83 | roadsterSchema.plugin(idPlugin); 84 | 85 | const Roadster = mongoose.model('Roadster', roadsterSchema); 86 | 87 | export default Roadster; 88 | -------------------------------------------------------------------------------- /docs/payloads/v4/all.md: -------------------------------------------------------------------------------- 1 | # Get all payloads 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/payloads` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "dragon": { 17 | "capsule": null, 18 | "mass_returned_kg": null, 19 | "mass_returned_lbs": null, 20 | "flight_time_sec": null, 21 | "manifest": null, 22 | "water_landing": null, 23 | "land_landing": null 24 | }, 25 | "name": "Tintin A & B", 26 | "type": "Satellite", 27 | "reused": false, 28 | "launch": "5eb87d14ffd86e000604b361", 29 | "customers": [ 30 | "SpaceX" 31 | ], 32 | "norad_ids": [ 33 | 43216, 34 | 43217 35 | ], 36 | "nationalities": [ 37 | "United States" 38 | ], 39 | "manufacturers": [ 40 | "SpaceX" 41 | ], 42 | "mass_kg": 800, 43 | "mass_lbs": 1763.7, 44 | "orbit": "SSO", 45 | "reference_system": "geocentric", 46 | "regime": "low-earth", 47 | "longitude": null, 48 | "semi_major_axis_km": 6737.42, 49 | "eccentricity": 0.0012995, 50 | "periapsis_km": 350.53, 51 | "apoapsis_km": 368.04, 52 | "inclination_deg": 97.4444, 53 | "period_min": 91.727, 54 | "lifespan_years": 1, 55 | "epoch": "2020-06-13T13:46:31.000Z", 56 | "mean_motion": 15.69864906, 57 | "raan": 176.6734, 58 | "arg_of_pericenter": 174.2326, 59 | "mean_anomaly": 185.9087, 60 | "id": "5eb0e4c6b6c3bb0006eeb21e" 61 | }, 62 | ... 63 | ] 64 | ``` 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # General 61 | *.DS_Store 62 | .AppleDouble 63 | .LSOverride 64 | 65 | # Icon must end with two \r 66 | Icon 67 | 68 | 69 | # Thumbnails 70 | ._* 71 | 72 | # Files that might appear in the root of a volume 73 | .DocumentRevisions-V100 74 | .fseventsd 75 | .Spotlight-V100 76 | .TemporaryItems 77 | .Trashes 78 | .VolumeIcon.icns 79 | .com.apple.timemachine.donotpresent 80 | 81 | # Directories potentially created on remote AFP share 82 | .AppleDB 83 | .AppleDesktop 84 | Network Trash Folder 85 | Temporary Items 86 | .apdisk 87 | 88 | # JetBrains IDE project files 89 | /.idea 90 | 91 | TODO.md 92 | test.json 93 | spacex-api 94 | .envrc -------------------------------------------------------------------------------- /docs/payloads/v4/one.md: -------------------------------------------------------------------------------- 1 | # Get one payload 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/payloads/:id` 6 | 7 | **URL Parameters** : `id=[string]` where `id` is the ID of the payload 8 | 9 | **Auth required** : `False` 10 | 11 | ## Success Response 12 | 13 | **Code** : `200 OK` 14 | 15 | **Content example** : 16 | 17 | ```json 18 | { 19 | "dragon": { 20 | "capsule": null, 21 | "mass_returned_kg": null, 22 | "mass_returned_lbs": null, 23 | "flight_time_sec": null, 24 | "manifest": null, 25 | "water_landing": null, 26 | "land_landing": null 27 | }, 28 | "name": "Tintin A & B", 29 | "type": "Satellite", 30 | "reused": false, 31 | "launch": "5eb87d14ffd86e000604b361", 32 | "customers": [ 33 | "SpaceX" 34 | ], 35 | "norad_ids": [ 36 | 43216, 37 | 43217 38 | ], 39 | "nationalities": [ 40 | "United States" 41 | ], 42 | "manufacturers": [ 43 | "SpaceX" 44 | ], 45 | "mass_kg": 800, 46 | "mass_lbs": 1763.7, 47 | "orbit": "SSO", 48 | "reference_system": "geocentric", 49 | "regime": "low-earth", 50 | "longitude": null, 51 | "semi_major_axis_km": 6737.42, 52 | "eccentricity": 0.0012995, 53 | "periapsis_km": 350.53, 54 | "apoapsis_km": 368.04, 55 | "inclination_deg": 97.4444, 56 | "period_min": 91.727, 57 | "lifespan_years": 1, 58 | "epoch": "2020-06-13T13:46:31.000Z", 59 | "mean_motion": 15.69864906, 60 | "raan": 176.6734, 61 | "arg_of_pericenter": 174.2326, 62 | "mean_anomaly": 185.9087, 63 | "id": "5eb0e4c6b6c3bb0006eeb21e" 64 | } 65 | ``` 66 | 67 | ## Error Responses 68 | 69 | **Code** : `404 NOT FOUND` 70 | 71 | **Content** : `Not Found` 72 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master, ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 4 * * 2' 11 | 12 | jobs: 13 | analyse: 14 | name: Analyse 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v1 33 | # Override language selection by uncommenting this and choosing your languages 34 | # with: 35 | # languages: go, javascript, csharp, python, cpp, java 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | - name: Autobuild 40 | uses: github/codeql-action/autobuild@v1 41 | 42 | # ℹ️ Command-line programs to run using the OS shell. 43 | # 📚 https://git.io/JvXDl 44 | 45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 46 | # and modify them (or add more) to build your code if your project 47 | # uses a compiled language 48 | 49 | #- run: | 50 | # make bootstrap 51 | # make release 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v1 55 | -------------------------------------------------------------------------------- /docs/ships/v4/schema.md: -------------------------------------------------------------------------------- 1 | # Ship Schema 2 | 3 | ```json 4 | { 5 | "name": { 6 | "type": "String", 7 | "unique": true, 8 | "required": true 9 | }, 10 | "legacy_id": { 11 | "type": "String", 12 | "default": null 13 | }, 14 | "model": { 15 | "type": "String", 16 | "default": null 17 | }, 18 | "type": { 19 | "type": "String", 20 | "default": null 21 | }, 22 | "roles": [ 23 | "String" 24 | ], 25 | "active": { 26 | "type": "Boolean", 27 | "required": true 28 | }, 29 | "imo": { 30 | "type": "Number", 31 | "default": null 32 | }, 33 | "mmsi": { 34 | "type": "Number", 35 | "default": null 36 | }, 37 | "abs": { 38 | "type": "Number", 39 | "default": null 40 | }, 41 | "class": { 42 | "type": "Number", 43 | "default": null 44 | }, 45 | "mass_kg": { 46 | "type": "Number", 47 | "default": null 48 | }, 49 | "mass_lbs": { 50 | "type": "Number", 51 | "default": null 52 | }, 53 | "year_built": { 54 | "type": "Number", 55 | "default": null 56 | }, 57 | "home_port": { 58 | "type": "String", 59 | "default": null 60 | }, 61 | "status": { 62 | "type": "String", 63 | "default": null 64 | }, 65 | "speed_kn": { 66 | "type": "Number", 67 | "default": null 68 | }, 69 | "course_deg": { 70 | "type": "Number", 71 | "default": null 72 | }, 73 | "latitude": { 74 | "type": "Number", 75 | "default": null 76 | }, 77 | "longitude": { 78 | "type": "Number", 79 | "default": null 80 | }, 81 | "last_ais_update": { 82 | "type": "String", 83 | "default": null 84 | }, 85 | "link": { 86 | "type": "String", 87 | "default": null 88 | }, 89 | "image": { 90 | "type": "String", 91 | "default": null 92 | }, 93 | "launches": [ 94 | { 95 | "type": "UUID" 96 | } 97 | ] 98 | } 99 | ``` 100 | -------------------------------------------------------------------------------- /routes/cores/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { Core } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/cores', 7 | }); 8 | 9 | // Get all cores 10 | router.get('/', cache(300), async (ctx) => { 11 | try { 12 | const result = await Core.find({}); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Get one core 21 | router.get('/:id', cache(300), async (ctx) => { 22 | const result = await Core.findById(ctx.params.id); 23 | if (!result) { 24 | ctx.throw(404); 25 | } 26 | ctx.status = 200; 27 | ctx.body = result; 28 | }); 29 | 30 | // Query cores 31 | router.post('/query', cache(300), async (ctx) => { 32 | const { query = {}, options = {} } = ctx.request.body; 33 | try { 34 | const result = await Core.paginate(query, options); 35 | ctx.status = 200; 36 | ctx.body = result; 37 | } catch (error) { 38 | ctx.throw(400, error.message); 39 | } 40 | }); 41 | 42 | // Create core 43 | router.post('/', auth, authz('core:create'), async (ctx) => { 44 | try { 45 | const core = new Core(ctx.request.body); 46 | await core.save(); 47 | ctx.status = 201; 48 | } catch (error) { 49 | ctx.throw(400, error.message); 50 | } 51 | }); 52 | 53 | // Update core 54 | router.patch('/:id', auth, authz('core:update'), async (ctx) => { 55 | try { 56 | await Core.findByIdAndUpdate(ctx.params.id, ctx.request.body, { runValidators: true }); 57 | ctx.status = 200; 58 | } catch (error) { 59 | ctx.throw(400, error.message); 60 | } 61 | }); 62 | 63 | // Delete core 64 | router.delete('/:id', auth, authz('core:delete'), async (ctx) => { 65 | try { 66 | await Core.findByIdAndDelete(ctx.params.id); 67 | ctx.status = 200; 68 | } catch (error) { 69 | ctx.throw(400, error.message); 70 | } 71 | }); 72 | 73 | export default router; 74 | -------------------------------------------------------------------------------- /routes/ships/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { Ship } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/ships', 7 | }); 8 | 9 | // Get all ships 10 | router.get('/', cache(300), async (ctx) => { 11 | try { 12 | const result = await Ship.find({}); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Get one ship 21 | router.get('/:id', cache(300), async (ctx) => { 22 | const result = await Ship.findById(ctx.params.id); 23 | if (!result) { 24 | ctx.throw(404); 25 | } 26 | ctx.status = 200; 27 | ctx.body = result; 28 | }); 29 | 30 | // Query ships 31 | router.post('/query', cache(300), async (ctx) => { 32 | const { query = {}, options = {} } = ctx.request.body; 33 | try { 34 | const result = await Ship.paginate(query, options); 35 | ctx.status = 200; 36 | ctx.body = result; 37 | } catch (error) { 38 | ctx.throw(400, error.message); 39 | } 40 | }); 41 | 42 | // Create a ship 43 | router.post('/', auth, authz('ship:create'), async (ctx) => { 44 | try { 45 | const ship = new Ship(ctx.request.body); 46 | await ship.save(); 47 | ctx.status = 201; 48 | } catch (error) { 49 | ctx.throw(400, error.message); 50 | } 51 | }); 52 | 53 | // Update a ship 54 | router.patch('/:id', auth, authz('ship:update'), async (ctx) => { 55 | try { 56 | await Ship.findByIdAndUpdate(ctx.params.id, ctx.request.body, { runValidators: true }); 57 | ctx.status = 200; 58 | } catch (error) { 59 | ctx.throw(400, error.message); 60 | } 61 | }); 62 | 63 | // Delete a ship 64 | router.delete('/:id', auth, authz('ship:delete'), async (ctx) => { 65 | try { 66 | await Ship.findByIdAndDelete(ctx.params.id); 67 | ctx.status = 200; 68 | } catch (error) { 69 | ctx.throw(400, error.message); 70 | } 71 | }); 72 | 73 | export default router; 74 | -------------------------------------------------------------------------------- /docs/ships/v4/query.md: -------------------------------------------------------------------------------- 1 | # Query ships 2 | 3 | **Method** : `POST` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/ships/query` 6 | 7 | **Auth required** : `False` 8 | 9 | **Body** : 10 | 11 | See [query](../../queries.md) guide for more details on building queries and paginating results. 12 | 13 | ```json 14 | { 15 | "query": {}, 16 | "options": {} 17 | } 18 | ``` 19 | 20 | ## Success Response 21 | 22 | **Code** : `200 OK` 23 | 24 | **Content example** : 25 | 26 | ```json 27 | { 28 | "docs": [ 29 | { 30 | "legacy_id": "GOPURSUIT", 31 | "model": null, 32 | "type": "Cargo", 33 | "roles": [ 34 | "Support Ship", 35 | "Fairing Recovery" 36 | ], 37 | "imo": 9458884, 38 | "mmsi": 367191410, 39 | "abs": 1201189, 40 | "class": 7174230, 41 | "mass_kg": 502999, 42 | "mass_lbs": 1108925, 43 | "year_built": 2007, 44 | "home_port": "Port Canaveral", 45 | "status": "", 46 | "speed_kn": null, 47 | "course_deg": null, 48 | "latitude": null, 49 | "longitude": null, 50 | "last_ais_update": null, 51 | "link": "https://www.marinetraffic.com/en/ais/details/ships/shipid:439594/mmsi:367191410/imo:9458884/vessel:GO_PURSUIT", 52 | "image": "https://i.imgur.com/5w1ZWre.jpg", 53 | "launches": [ 54 | "5eb87d18ffd86e000604b365", 55 | "5eb87d19ffd86e000604b366", 56 | "5eb87d1bffd86e000604b368", 57 | "5eb87d1effd86e000604b36a" 58 | ], 59 | "name": "GO Pursuit", 60 | "active": false, 61 | "id": "5ea6ed2e080df4000697c90a" 62 | }, 63 | ... 64 | ], 65 | "totalDocs": 22, 66 | "offset": 0, 67 | "limit": 10, 68 | "totalPages": 3, 69 | "page": 1, 70 | "pagingCounter": 1, 71 | "hasPrevPage": false, 72 | "hasNextPage": true, 73 | "prevPage": null, 74 | "nextPage": 2 75 | } 76 | ``` 77 | 78 | ## Error Responses 79 | 80 | **Code** : `400 Bad Request` 81 | 82 | **Content** : Mongoose error is shown, with suggestions to fix the query. 83 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | # Publish `master` as Docker `latest` image. 6 | branches: 7 | - master 8 | 9 | # Publish `v1.2.3` tags as releases. 10 | tags: 11 | - v* 12 | pull_request: 13 | 14 | env: 15 | IMAGE_NAME: spacex-api 16 | 17 | jobs: 18 | test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-node@v1 23 | with: 24 | node-version: "18.x" 25 | - run: npm install 26 | - run: npm run check-dependencies 27 | - run: npm test 28 | 29 | # Push image to GitHub Packages. 30 | # See also https://docs.docker.com/docker-hub/builds/ 31 | push: 32 | # Ensure test job passes before pushing image. 33 | needs: test 34 | 35 | runs-on: ubuntu-latest 36 | if: github.event_name == 'push' 37 | 38 | steps: 39 | - uses: actions/checkout@v2 40 | 41 | - name: Build image 42 | run: docker build . --file Dockerfile --tag $IMAGE_NAME 43 | 44 | - name: Log into registry 45 | run: echo "${{ secrets.GITLAB_PASSWORD }}" | docker login registry.gitlab.com -u ${{ secrets.GITLAB_USERNAME }} --password-stdin 46 | 47 | - name: Push image 48 | run: | 49 | IMAGE_ID=registry.gitlab.com/jakewmeyer/spacex-api 50 | 51 | # Change all uppercase to lowercase 52 | IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') 53 | 54 | # Strip git ref prefix from version 55 | VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') 56 | 57 | # Strip "v" prefix from tag name 58 | [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') 59 | 60 | # Use Docker `latest` tag convention 61 | [ "$VERSION" == "master" ] && VERSION=latest 62 | 63 | echo IMAGE_ID=$IMAGE_ID 64 | echo VERSION=$VERSION 65 | 66 | docker tag $IMAGE_NAME $IMAGE_ID:$VERSION 67 | docker push $IMAGE_ID:$VERSION 68 | -------------------------------------------------------------------------------- /routes/crew/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { Crew } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/crew', 7 | }); 8 | 9 | // Get all crew 10 | router.get('/', cache(300), async (ctx) => { 11 | try { 12 | const result = await Crew.find({}); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Get one crew member 21 | router.get('/:id', cache(300), async (ctx) => { 22 | const result = await Crew.findById(ctx.params.id); 23 | if (!result) { 24 | ctx.throw(404); 25 | } 26 | ctx.status = 200; 27 | ctx.body = result; 28 | }); 29 | 30 | // Query crew members 31 | router.post('/query', cache(300), async (ctx) => { 32 | const { query = {}, options = {} } = ctx.request.body; 33 | try { 34 | const result = await Crew.paginate(query, options); 35 | ctx.status = 200; 36 | ctx.body = result; 37 | } catch (error) { 38 | ctx.throw(400, error.message); 39 | } 40 | }); 41 | 42 | // Create crew member 43 | router.post('/', auth, authz('crew:create'), async (ctx) => { 44 | try { 45 | const crew = new Crew(ctx.request.body); 46 | await crew.save(); 47 | ctx.status = 201; 48 | } catch (error) { 49 | ctx.throw(400, error.message); 50 | } 51 | }); 52 | 53 | // Update crew member 54 | router.patch('/:id', auth, authz('crew:update'), async (ctx) => { 55 | try { 56 | await Crew.findByIdAndUpdate(ctx.params.id, ctx.request.body, { runValidators: true }); 57 | ctx.status = 200; 58 | } catch (error) { 59 | ctx.throw(400, error.message); 60 | } 61 | }); 62 | 63 | // Delete crew member 64 | router.delete('/:id', auth, authz('crew:delete'), async (ctx) => { 65 | try { 66 | await Crew.findByIdAndDelete(ctx.params.id); 67 | ctx.status = 200; 68 | } catch (error) { 69 | ctx.throw(400, error.message); 70 | } 71 | }); 72 | 73 | export default router; 74 | -------------------------------------------------------------------------------- /routes/capsules/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { Capsule } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/capsules', 7 | }); 8 | 9 | // Get all capsules 10 | router.get('/', cache(300), async (ctx) => { 11 | try { 12 | const result = await Capsule.find({}); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Get one capsule 21 | router.get('/:id', cache(300), async (ctx) => { 22 | const result = await Capsule.findById(ctx.params.id); 23 | if (!result) { 24 | ctx.throw(404); 25 | } 26 | ctx.status = 200; 27 | ctx.body = result; 28 | }); 29 | 30 | // Query capsules 31 | router.post('/query', cache(300), async (ctx) => { 32 | const { query = {}, options = {} } = ctx.request.body; 33 | try { 34 | const result = await Capsule.paginate(query, options); 35 | ctx.status = 200; 36 | ctx.body = result; 37 | } catch (error) { 38 | ctx.throw(400, error.message); 39 | } 40 | }); 41 | 42 | // Create capsule 43 | router.post('/', auth, authz('capsule:create'), async (ctx) => { 44 | try { 45 | const capsule = new Capsule(ctx.request.body); 46 | await capsule.save(); 47 | ctx.status = 201; 48 | } catch (error) { 49 | ctx.throw(400, error.message); 50 | } 51 | }); 52 | 53 | // Update capsule 54 | router.patch('/:id', auth, authz('capsule:update'), async (ctx) => { 55 | try { 56 | await Capsule.findByIdAndUpdate(ctx.params.id, ctx.request.body, { runValidators: true }); 57 | ctx.status = 200; 58 | } catch (error) { 59 | ctx.throw(400, error.message); 60 | } 61 | }); 62 | 63 | // Delete capsule 64 | router.delete('/:id', auth, authz('capsule:delete'), async (ctx) => { 65 | try { 66 | await Capsule.findByIdAndDelete(ctx.params.id); 67 | ctx.status = 200; 68 | } catch (error) { 69 | ctx.throw(400, error.message); 70 | } 71 | }); 72 | 73 | export default router; 74 | -------------------------------------------------------------------------------- /routes/landpads/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { Landpad } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/landpads', 7 | }); 8 | 9 | // Get all landpads 10 | router.get('/', cache(300), async (ctx) => { 11 | try { 12 | const result = await Landpad.find({}); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Get one landpad 21 | router.get('/:id', cache(300), async (ctx) => { 22 | const result = await Landpad.findById(ctx.params.id); 23 | if (!result) { 24 | ctx.throw(404); 25 | } 26 | ctx.status = 200; 27 | ctx.body = result; 28 | }); 29 | 30 | // Query landpads 31 | router.post('/query', cache(300), async (ctx) => { 32 | const { query = {}, options = {} } = ctx.request.body; 33 | try { 34 | const result = await Landpad.paginate(query, options); 35 | ctx.status = 200; 36 | ctx.body = result; 37 | } catch (error) { 38 | ctx.throw(400, error.message); 39 | } 40 | }); 41 | 42 | // Create a landpad 43 | router.post('/', auth, authz('landpad:create'), async (ctx) => { 44 | try { 45 | const landpad = new Landpad(ctx.request.body); 46 | await landpad.save(); 47 | ctx.status = 201; 48 | } catch (error) { 49 | ctx.throw(400, error.message); 50 | } 51 | }); 52 | 53 | // Update a landpad 54 | router.patch('/:id', auth, authz('landpad:update'), async (ctx) => { 55 | try { 56 | await Landpad.findByIdAndUpdate(ctx.params.id, ctx.request.body, { runValidators: true }); 57 | ctx.status = 200; 58 | } catch (error) { 59 | ctx.throw(400, error.message); 60 | } 61 | }); 62 | 63 | // Delete landpad 64 | router.delete('/:id', auth, authz('landpad:delete'), async (ctx) => { 65 | try { 66 | await Landpad.findByIdAndDelete(ctx.params.id); 67 | ctx.status = 200; 68 | } catch (error) { 69 | ctx.throw(400, error.message); 70 | } 71 | }); 72 | 73 | export default router; 74 | -------------------------------------------------------------------------------- /routes/users/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { User } from '../../../models/index.js'; 3 | import { auth, authz } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/users', 7 | }); 8 | 9 | // Get all users 10 | router.get('/', auth, authz('user:list'), async (ctx) => { 11 | try { 12 | const result = await User.find({}); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Get one user 21 | router.get('/:id', auth, authz('user:one'), async (ctx) => { 22 | const result = await User.findById(ctx.params.id); 23 | if (!result) { 24 | ctx.throw(404); 25 | } 26 | ctx.status = 200; 27 | ctx.body = result; 28 | }); 29 | 30 | // Query users 31 | router.post('/query', auth, authz('user:query'), async (ctx) => { 32 | const { query = {}, options = {} } = ctx.request.body; 33 | try { 34 | const result = await User.paginate(query, options); 35 | ctx.status = 200; 36 | ctx.body = result; 37 | } catch (error) { 38 | ctx.throw(400, error.message); 39 | } 40 | }); 41 | 42 | // Create a user 43 | router.post('/', auth, authz('user:create'), async (ctx) => { 44 | try { 45 | const user = new User(ctx.request.body); 46 | await user.save(); 47 | ctx.status = 201; 48 | } catch (error) { 49 | ctx.throw(400, error.message); 50 | } 51 | }); 52 | 53 | // Update a user 54 | router.patch('/:id', auth, authz('user:update'), async (ctx) => { 55 | try { 56 | await User.findByIdAndUpdate(ctx.params.id, ctx.request.body, { 57 | runValidators: true, 58 | }); 59 | ctx.status = 200; 60 | } catch (error) { 61 | ctx.throw(400, error.message); 62 | } 63 | }); 64 | 65 | // Delete a user 66 | router.delete('/:id', auth, authz('user:delete'), async (ctx) => { 67 | try { 68 | await User.findByIdAndDelete(ctx.params.id); 69 | ctx.status = 200; 70 | } catch (error) { 71 | ctx.throw(400, error.message); 72 | } 73 | }); 74 | 75 | export default router; 76 | -------------------------------------------------------------------------------- /routes/dragons/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { Dragon } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/dragons', 7 | }); 8 | 9 | // Get all dragons 10 | router.get('/', cache(86400), async (ctx) => { 11 | try { 12 | const result = await Dragon.find({}, null, { sort: { name: 'asc' } }); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Get one dragon 21 | router.get('/:id', cache(86400), async (ctx) => { 22 | const result = await Dragon.findById(ctx.params.id); 23 | if (!result) { 24 | ctx.throw(404); 25 | } 26 | ctx.status = 200; 27 | ctx.body = result; 28 | }); 29 | 30 | // Query dragons 31 | router.post('/query', cache(86400), async (ctx) => { 32 | const { query = {}, options = {} } = ctx.request.body; 33 | try { 34 | const result = await Dragon.paginate(query, options); 35 | ctx.status = 200; 36 | ctx.body = result; 37 | } catch (error) { 38 | ctx.throw(400, error.message); 39 | } 40 | }); 41 | 42 | // Create a dragon 43 | router.post('/', auth, authz('dragon:create'), async (ctx) => { 44 | try { 45 | const dragon = new Dragon(ctx.request.body); 46 | await dragon.save(); 47 | ctx.status = 201; 48 | } catch (error) { 49 | ctx.throw(400, error.message); 50 | } 51 | }); 52 | 53 | // Update a dragon 54 | router.patch('/:id', auth, authz('dragon:update'), async (ctx) => { 55 | try { 56 | await Dragon.findByIdAndUpdate(ctx.params.id, ctx.request.body, { runValidators: true }); 57 | ctx.status = 200; 58 | } catch (error) { 59 | ctx.throw(400, error.message); 60 | } 61 | }); 62 | 63 | // Delete dragon 64 | router.delete('/:id', auth, authz('dragon:delete'), async (ctx) => { 65 | try { 66 | await Dragon.findByIdAndDelete(ctx.params.id); 67 | ctx.status = 200; 68 | } catch (error) { 69 | ctx.throw(400, error.message); 70 | } 71 | }); 72 | 73 | export default router; 74 | -------------------------------------------------------------------------------- /routes/payloads/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { Payload } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/payloads', 7 | }); 8 | 9 | // Get all payloads 10 | router.get('/', cache(300), async (ctx) => { 11 | try { 12 | const result = await Payload.find({}); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Get one payload 21 | router.get('/:id', cache(300), async (ctx) => { 22 | const result = await Payload.findById(ctx.params.id); 23 | if (!result) { 24 | ctx.throw(404); 25 | } 26 | ctx.status = 200; 27 | ctx.body = result; 28 | }); 29 | 30 | // Query payloads 31 | router.post('/query', cache(300), async (ctx) => { 32 | const { query = {}, options = {} } = ctx.request.body; 33 | try { 34 | const result = await Payload.paginate(query, options); 35 | ctx.status = 200; 36 | ctx.body = result; 37 | } catch (error) { 38 | ctx.throw(400, error.message); 39 | } 40 | }); 41 | 42 | // Create a payload 43 | router.post('/', auth, authz('payload:create'), async (ctx) => { 44 | try { 45 | const payload = new Payload(ctx.request.body); 46 | await payload.save(); 47 | ctx.status = 201; 48 | } catch (error) { 49 | ctx.throw(400, error.message); 50 | } 51 | }); 52 | 53 | // Update a payload 54 | router.patch('/:id', auth, authz('payload:update'), async (ctx) => { 55 | try { 56 | await Payload.findByIdAndUpdate(ctx.params.id, ctx.request.body, { 57 | runValidators: true, 58 | }); 59 | ctx.status = 200; 60 | } catch (error) { 61 | ctx.throw(400, error.message); 62 | } 63 | }); 64 | 65 | // Delete a payload 66 | router.delete('/:id', auth, authz('payload:delete'), async (ctx) => { 67 | try { 68 | await Payload.findByIdAndDelete(ctx.params.id); 69 | ctx.status = 200; 70 | } catch (error) { 71 | ctx.throw(400, error.message); 72 | } 73 | }); 74 | 75 | export default router; 76 | -------------------------------------------------------------------------------- /routes/rockets/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { Rocket } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/rockets', 7 | }); 8 | 9 | // Get all rockets 10 | router.get('/', cache(86400), async (ctx) => { 11 | try { 12 | const result = await Rocket.find({}, null, { sort: { first_flight: 'asc' } }); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Get one rocket 21 | router.get('/:id', cache(86400), async (ctx) => { 22 | const result = await Rocket.findById(ctx.params.id); 23 | if (!result) { 24 | ctx.throw(404); 25 | } 26 | ctx.status = 200; 27 | ctx.body = result; 28 | }); 29 | 30 | // Query rocket 31 | router.post('/query', cache(300), async (ctx) => { 32 | const { query = {}, options = {} } = ctx.request.body; 33 | try { 34 | const result = await Rocket.paginate(query, options); 35 | ctx.status = 200; 36 | ctx.body = result; 37 | } catch (error) { 38 | ctx.throw(400, error.message); 39 | } 40 | }); 41 | 42 | // Create a rocket 43 | router.post('/', auth, authz('rocket:create'), async (ctx) => { 44 | try { 45 | const rocket = new Rocket(ctx.request.body); 46 | await rocket.save(); 47 | ctx.status = 201; 48 | } catch (error) { 49 | ctx.throw(400, error.message); 50 | } 51 | }); 52 | 53 | // Update a rocket 54 | router.patch('/:id', auth, authz('rocket:update'), async (ctx) => { 55 | try { 56 | await Rocket.findByIdAndUpdate(ctx.params.id, ctx.request.body, { runValidators: true }); 57 | ctx.status = 200; 58 | } catch (error) { 59 | ctx.throw(400, error.message); 60 | } 61 | }); 62 | 63 | // Delete a rocket 64 | router.delete('/:id', auth, authz('rocket:delete'), async (ctx) => { 65 | try { 66 | await Rocket.findByIdAndDelete(ctx.params.id); 67 | ctx.status = 200; 68 | } catch (error) { 69 | ctx.throw(400, error.message); 70 | } 71 | }); 72 | 73 | export default router; 74 | -------------------------------------------------------------------------------- /routes/history/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { History } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/history', 7 | }); 8 | 9 | // Get all history events 10 | router.get('/', cache(300), async (ctx) => { 11 | try { 12 | const result = await History.find({}); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Get one history event 21 | router.get('/:id', cache(300), async (ctx) => { 22 | const result = await History.findById(ctx.params.id); 23 | if (!result) { 24 | ctx.throw(404); 25 | } 26 | ctx.status = 200; 27 | ctx.body = result; 28 | }); 29 | 30 | // Query history events 31 | router.post('/query', cache(300), async (ctx) => { 32 | const { query = {}, options = {} } = ctx.request.body; 33 | try { 34 | const result = await History.paginate(query, options); 35 | ctx.status = 200; 36 | ctx.body = result; 37 | } catch (error) { 38 | ctx.throw(400, error.message); 39 | } 40 | }); 41 | 42 | // Create a history event 43 | router.post('/', auth, authz('history:create'), async (ctx) => { 44 | try { 45 | const history = new History(ctx.request.body); 46 | await history.save(); 47 | ctx.status = 201; 48 | } catch (error) { 49 | ctx.throw(400, error.message); 50 | } 51 | }); 52 | 53 | // Update a history event 54 | router.patch('/:id', auth, authz('history:update'), async (ctx) => { 55 | try { 56 | await History.findByIdAndUpdate(ctx.params.id, ctx.request.body, { runValidators: true }); 57 | ctx.status = 200; 58 | } catch (error) { 59 | ctx.throw(400, error.message); 60 | } 61 | }); 62 | 63 | // Delete history event 64 | router.delete('/:id', auth, authz('history:delete'), async (ctx) => { 65 | try { 66 | await History.findByIdAndDelete(ctx.params.id); 67 | ctx.status = 200; 68 | } catch (error) { 69 | ctx.throw(400, error.message); 70 | } 71 | }); 72 | 73 | export default router; 74 | -------------------------------------------------------------------------------- /routes/launchpads/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { Launchpad } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/launchpads', 7 | }); 8 | 9 | // Get all launchpads 10 | router.get('/', cache(300), async (ctx) => { 11 | try { 12 | const result = await Launchpad.find({}); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Get one launchpad 21 | router.get('/:id', cache(300), async (ctx) => { 22 | const result = await Launchpad.findById(ctx.params.id); 23 | if (!result) { 24 | ctx.throw(404); 25 | } 26 | ctx.status = 200; 27 | ctx.body = result; 28 | }); 29 | 30 | // Query launchpads 31 | router.post('/query', cache(300), async (ctx) => { 32 | const { query = {}, options = {} } = ctx.request.body; 33 | try { 34 | const result = await Launchpad.paginate(query, options); 35 | ctx.status = 200; 36 | ctx.body = result; 37 | } catch (error) { 38 | ctx.throw(400, error.message); 39 | } 40 | }); 41 | 42 | // Create a launchpad 43 | router.post('/', auth, authz('launchpad:create'), async (ctx) => { 44 | try { 45 | const launchpad = new Launchpad(ctx.request.body); 46 | await launchpad.save(); 47 | ctx.status = 201; 48 | } catch (error) { 49 | ctx.throw(400, error.message); 50 | } 51 | }); 52 | 53 | // Update a launchpad 54 | router.patch('/:id', auth, authz('launchpad:update'), async (ctx) => { 55 | try { 56 | await Launchpad.findByIdAndUpdate(ctx.params.id, ctx.request.body, { runValidators: true }); 57 | ctx.status = 200; 58 | } catch (error) { 59 | ctx.throw(400, error.message); 60 | } 61 | }); 62 | 63 | // Delete a launchpad 64 | router.delete('/:id', auth, authz('launchpad:delete'), async (ctx) => { 65 | try { 66 | await Launchpad.findByIdAndDelete(ctx.params.id); 67 | ctx.status = 200; 68 | } catch (error) { 69 | ctx.throw(400, error.message); 70 | } 71 | }); 72 | 73 | export default router; 74 | -------------------------------------------------------------------------------- /docs/roadster/v4/get.md: -------------------------------------------------------------------------------- 1 | # Get roadster info 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/roadster` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | { 15 | "flickr_images": [ 16 | "https://farm5.staticflickr.com/4615/40143096241_11128929df_b.jpg", 17 | "https://farm5.staticflickr.com/4702/40110298232_91b32d0cc0_b.jpg", 18 | "https://farm5.staticflickr.com/4676/40110297852_5e794b3258_b.jpg", 19 | "https://farm5.staticflickr.com/4745/40110304192_6e3e9a7a1b_b.jpg" 20 | ], 21 | "name": "Elon Musk's Tesla Roadster", 22 | "launch_date_utc": "2018-02-06T20:45:00.000Z", 23 | "launch_date_unix": 1517949900, 24 | "launch_mass_kg": 1350, 25 | "launch_mass_lbs": 2976, 26 | "norad_id": 43205, 27 | "epoch_jd": 2459014.345891204, 28 | "orbit_type": "heliocentric", 29 | "apoapsis_au": 1.663950009802517, 30 | "periapsis_au": 0.9859657216725529, 31 | "semi_major_axis_au": 196.2991348009594, 32 | "eccentricity": 0.2558512635239784, 33 | "inclination": 1.077499248052439, 34 | "longitude": 317.0839961949045, 35 | "periapsis_arg": 177.5240278992875, 36 | "period_days": 557.059427465354, 37 | "speed_kph": 72209.97792, 38 | "speed_mph": 44869.18619012833, 39 | "earth_distance_km": 220606726.83228922, 40 | "earth_distance_mi": 137078622.45850638, 41 | "mars_distance_km": 89348334.47067611, 42 | "mars_distance_mi": 55518463.93837848, 43 | "wikipedia": "https://en.wikipedia.org/wiki/Elon_Musk%27s_Tesla_Roadster", 44 | "video": "https://youtu.be/wbSwFU6tY1c", 45 | "details": "Elon Musk's Tesla Roadster is an electric sports car that served as the dummy payload for the February 2018 Falcon Heavy test flight and is now an artificial satellite of the Sun. Starman, a mannequin dressed in a spacesuit, occupies the driver's seat. The car and rocket are products of Tesla and SpaceX. This 2008-model Roadster was previously used by Musk for commuting, and is the only consumer car sent into space.", 46 | "id": "5eb75f0842fea42237d7f3f4" 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /jobs/launchpads.js: -------------------------------------------------------------------------------- 1 | import got from 'got'; 2 | import { logger } from '../middleware/index.js'; 3 | 4 | const API = process.env.SPACEX_API; 5 | const KEY = process.env.SPACEX_KEY; 6 | const HEALTHCHECK = process.env.LAUNCHPADS_HEALTHCHECK; 7 | 8 | /** 9 | * Update launchpad attempts/successes 10 | * @return {Promise} 11 | */ 12 | export default async () => { 13 | try { 14 | const launchpads = await got.post(`${API}/launchpads/query`, { 15 | json: { 16 | options: { 17 | pagination: false, 18 | }, 19 | }, 20 | resolveBodyOnly: true, 21 | responseType: 'json', 22 | }); 23 | 24 | const updates = launchpads.docs.map(async (launchpad) => { 25 | const [attempts, successes] = await Promise.all([ 26 | got.post(`${API}/launches/query`, { 27 | json: { 28 | query: { 29 | launchpad: launchpad.id, 30 | upcoming: false, 31 | }, 32 | options: { 33 | pagination: false, 34 | }, 35 | }, 36 | resolveBodyOnly: true, 37 | responseType: 'json', 38 | }), 39 | got.post(`${API}/launches/query`, { 40 | json: { 41 | query: { 42 | launchpad: launchpad.id, 43 | upcoming: false, 44 | success: true, 45 | }, 46 | options: { 47 | pagination: false, 48 | }, 49 | }, 50 | resolveBodyOnly: true, 51 | responseType: 'json', 52 | }), 53 | ]); 54 | 55 | await got.patch(`${API}/launchpads/${launchpad.id}`, { 56 | json: { 57 | launch_attempts: attempts.totalDocs, 58 | launch_successes: successes.totalDocs, 59 | }, 60 | headers: { 61 | 'spacex-key': KEY, 62 | }, 63 | }); 64 | }); 65 | 66 | await Promise.all(updates); 67 | 68 | logger.info('Launchpads updated'); 69 | 70 | if (HEALTHCHECK) { 71 | await got(HEALTHCHECK); 72 | } 73 | } catch (error) { 74 | console.log(`Launchpads Error: ${error.message}`); 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /jobs/worker.js: -------------------------------------------------------------------------------- 1 | import { CronJob } from 'cron'; 2 | import dotenv from 'dotenv'; 3 | import { logger } from '../middleware/index.js'; 4 | import launches from './launches.js'; 5 | import payloads from './payloads.js'; 6 | import landpads from './landpads.js'; 7 | import launchpads from './launchpads.js'; 8 | import capsules from './capsules.js'; 9 | import cores from './cores.js'; 10 | import roadster from './roadster.js'; 11 | import upcoming from './upcoming.js'; 12 | import starlink from './starlink.js'; 13 | import webcast from './webcast.js'; 14 | import launchLibrary from './launch-library.js'; 15 | 16 | // Env init 17 | dotenv.config(); 18 | 19 | // Every 10 minutes 20 | const launchesJob = new CronJob('*/10 * * * *', launches); 21 | 22 | // Every 10 minutes 23 | const landpadsJob = new CronJob('*/10 * * * *', landpads); 24 | 25 | // Every 10 minutes 26 | const launchpadsJob = new CronJob('*/10 * * * *', launchpads); 27 | 28 | // Every 10 minutes 29 | const capsulesJob = new CronJob('*/10 * * * *', capsules); 30 | 31 | // Every 10 minutes 32 | const coresJob = new CronJob('*/10 * * * *', cores); 33 | 34 | // Every 10 minutes 35 | const roadsterJob = new CronJob('*/10 * * * *', roadster); 36 | 37 | // Every 10 minutes 38 | const upcomingJob = new CronJob('*/10 * * * *', upcoming); 39 | 40 | // Every hour on :25 41 | const payloadsJob = new CronJob('25 * * * *', payloads); 42 | 43 | // Every hour on :35 44 | const starlinkJob = new CronJob('35 * * * *', starlink); 45 | 46 | // Every 5 minutes 47 | const webcastJob = new CronJob('*/5 * * * *', webcast); 48 | 49 | // Every hour on :45 50 | const launchLibraryJob = new CronJob('45 * * * *', launchLibrary); 51 | 52 | try { 53 | launchesJob.start(); 54 | payloadsJob.start(); 55 | landpadsJob.start(); 56 | launchpadsJob.start(); 57 | capsulesJob.start(); 58 | coresJob.start(); 59 | roadsterJob.start(); 60 | upcomingJob.start(); 61 | starlinkJob.start(); 62 | webcastJob.start(); 63 | launchLibraryJob.start(); 64 | } catch (error) { 65 | const formatted = { 66 | name: 'worker', 67 | error: error.message, 68 | stack: error.stack, 69 | }; 70 | logger.error(formatted); 71 | } 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spacex-api", 3 | "version": "4.0.0", 4 | "description": "Open Source REST API for data about SpaceX", 5 | "main": "server.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "npm run lint && npm run check-dependencies && jest --silent --verbose", 9 | "start": "node server.js", 10 | "worker": "node jobs/worker.js", 11 | "lint": "eslint .", 12 | "check-dependencies": "npx depcheck --ignores=\"pino-pretty\"" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/r-spacex/SpaceX-API" 17 | }, 18 | "keywords": [ 19 | "spacex", 20 | "space", 21 | "rocket", 22 | "rest-api", 23 | "mongodb", 24 | "koa" 25 | ], 26 | "author": "Jake Meyer ", 27 | "license": "Apache-2.0", 28 | "bugs": { 29 | "url": "https://github.com/r-spacex/SpaceX-API/issues" 30 | }, 31 | "homepage": "https://github.com/r-spacex/SpaceX-API", 32 | "dependencies": { 33 | "blake3": "^2.1.7", 34 | "cheerio": "^1.0.0-rc.12", 35 | "cron": "^2.1.0", 36 | "dotenv": "^16.0.1", 37 | "fuzzball": "^2.1.2", 38 | "got": "^12.3.1", 39 | "ioredis": "^5.2.3", 40 | "koa": "^2.13.4", 41 | "koa-bodyparser": "^4.3.0", 42 | "koa-conditional-get": "^3.0.0", 43 | "koa-etag": "^4.0.0", 44 | "koa-helmet": "^6.1.0", 45 | "koa-router": "^12.0.0", 46 | "koa2-cors": "^2.0.6", 47 | "lodash": "^4.17.21", 48 | "moment-range": "^4.0.2", 49 | "moment-timezone": "^0.5.37", 50 | "mongoose": "^6.5.3", 51 | "mongoose-id": "^0.1.3", 52 | "mongoose-paginate-v2": "^1.7.0", 53 | "pino": "^8.4.2", 54 | "rss-parser": "^3.12.0", 55 | "tle.js": "^4.7.1", 56 | "tough-cookie": "^4.1.2" 57 | }, 58 | "devDependencies": { 59 | "@typescript-eslint/eslint-plugin": "^5.35.1", 60 | "@typescript-eslint/parser": "^5.35.1", 61 | "eslint": "^8.23.0", 62 | "eslint-config-airbnb-base": "^15.0.0", 63 | "eslint-plugin-import": "^2.26.0", 64 | "eslint-plugin-jest": "^26.8.7", 65 | "jest": "^29.0.1" 66 | }, 67 | "engines": { 68 | "node": ">=14.16" 69 | }, 70 | "jest": { 71 | "testEnvironment": "node" 72 | }, 73 | "volta": { 74 | "node": "18.7.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /models/ships.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import mongoosePaginate from 'mongoose-paginate-v2'; 3 | import idPlugin from 'mongoose-id'; 4 | 5 | const shipSchema = new mongoose.Schema({ 6 | name: { 7 | type: String, 8 | unique: true, 9 | required: true, 10 | }, 11 | legacy_id: { 12 | type: String, 13 | default: null, 14 | }, 15 | model: { 16 | type: String, 17 | default: null, 18 | }, 19 | type: { 20 | type: String, 21 | default: null, 22 | }, 23 | roles: [ 24 | String, 25 | ], 26 | active: { 27 | type: Boolean, 28 | required: true, 29 | }, 30 | imo: { 31 | type: Number, 32 | default: null, 33 | }, 34 | mmsi: { 35 | type: Number, 36 | default: null, 37 | }, 38 | abs: { 39 | type: Number, 40 | default: null, 41 | }, 42 | class: { 43 | type: Number, 44 | default: null, 45 | }, 46 | mass_kg: { 47 | type: Number, 48 | default: null, 49 | }, 50 | mass_lbs: { 51 | type: Number, 52 | default: null, 53 | }, 54 | year_built: { 55 | type: Number, 56 | default: null, 57 | }, 58 | home_port: { 59 | type: String, 60 | default: null, 61 | }, 62 | status: { 63 | type: String, 64 | default: null, 65 | }, 66 | speed_kn: { 67 | type: Number, 68 | default: null, 69 | }, 70 | course_deg: { 71 | type: Number, 72 | default: null, 73 | }, 74 | latitude: { 75 | type: Number, 76 | default: null, 77 | }, 78 | longitude: { 79 | type: Number, 80 | default: null, 81 | }, 82 | last_ais_update: { 83 | type: String, 84 | default: null, 85 | }, 86 | link: { 87 | type: String, 88 | default: null, 89 | }, 90 | image: { 91 | type: String, 92 | default: null, 93 | }, 94 | launches: [{ 95 | type: mongoose.ObjectId, 96 | ref: 'Launch', 97 | }], 98 | }, { autoCreate: true }); 99 | 100 | const index = { 101 | name: 'text', 102 | }; 103 | shipSchema.index(index); 104 | 105 | shipSchema.plugin(mongoosePaginate); 106 | shipSchema.plugin(idPlugin); 107 | 108 | const Ship = mongoose.model('Ship', shipSchema); 109 | 110 | export default Ship; 111 | -------------------------------------------------------------------------------- /routes/starlink/v4/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { Starlink } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v4|latest)/starlink', 7 | }); 8 | 9 | // Get all Starlink satellites 10 | router.get('/', cache(3600), async (ctx) => { 11 | try { 12 | const result = await Starlink.find({}); 13 | ctx.status = 200; 14 | ctx.body = result; 15 | } catch (error) { 16 | ctx.throw(400, error.message); 17 | } 18 | }); 19 | 20 | // Get one Starlink satellite 21 | router.get('/:id', cache(3600), async (ctx) => { 22 | const result = await Starlink.findById(ctx.params.id); 23 | if (!result) { 24 | ctx.throw(404); 25 | } 26 | ctx.status = 200; 27 | ctx.body = result; 28 | }); 29 | 30 | // Query Starlink satellites 31 | router.post('/query', cache(3600), async (ctx) => { 32 | const { query = {}, options = {} } = ctx.request.body; 33 | try { 34 | const result = await Starlink.paginate(query, options); 35 | ctx.status = 200; 36 | ctx.body = result; 37 | } catch (error) { 38 | ctx.throw(400, error.message); 39 | } 40 | }); 41 | 42 | // Create a Starlink satellite 43 | router.post('/', auth, authz('starlink:create'), async (ctx) => { 44 | try { 45 | const ship = new Starlink(ctx.request.body); 46 | await ship.save(); 47 | ctx.status = 201; 48 | } catch (error) { 49 | ctx.throw(400, error.message); 50 | } 51 | }); 52 | 53 | // Update a Starlink satellite 54 | router.patch('/:norad_id', auth, authz('starlink:update'), async (ctx) => { 55 | try { 56 | await Starlink.findOneAndUpdate({ 'spaceTrack.NORAD_CAT_ID': ctx.params.norad_id }, ctx.request.body, { 57 | runValidators: true, 58 | setDefaultsOnInsert: true, 59 | upsert: true, 60 | }); 61 | ctx.status = 200; 62 | } catch (error) { 63 | ctx.throw(400, error.message); 64 | } 65 | }); 66 | 67 | // Delete a Starlink satellite 68 | router.delete('/:id', auth, authz('starlink:delete'), async (ctx) => { 69 | try { 70 | await Starlink.findByIdAndDelete(ctx.params.id); 71 | ctx.status = 200; 72 | } catch (error) { 73 | ctx.throw(400, error.message); 74 | } 75 | }); 76 | 77 | export default router; 78 | -------------------------------------------------------------------------------- /docs/launchpads/v4/query.md: -------------------------------------------------------------------------------- 1 | # Query launchpads 2 | 3 | **Method** : `POST` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/launchpads/query` 6 | 7 | **Auth required** : `False` 8 | 9 | **Body** : 10 | 11 | See [query](../../queries.md) guide for more details on building queries and paginating results. 12 | 13 | ```json 14 | { 15 | "query": {}, 16 | "options": {} 17 | } 18 | ``` 19 | 20 | ## Success Response 21 | 22 | **Code** : `200 OK` 23 | 24 | **Content example** : 25 | 26 | ```json 27 | { 28 | "docs": [ 29 | { 30 | "name": "VAFB SLC 4E", 31 | "full_name": "Vandenberg Air Force Base Space Launch Complex 4E", 32 | "locality": "Vandenberg Air Force Base", 33 | "region": "California", 34 | "timezone": "America/Los_Angeles", 35 | "latitude": 34.632093, 36 | "longitude": -120.610829, 37 | "launch_attempts": 15, 38 | "launch_successes": 15, 39 | "rockets": [ 40 | "5e9d0d95eda69973a809d1ec" 41 | ], 42 | "launches": [ 43 | "5eb87ce1ffd86e000604b334", 44 | "5eb87cf0ffd86e000604b343", 45 | "5eb87cfdffd86e000604b34c", 46 | "5eb87d05ffd86e000604b354", 47 | "5eb87d08ffd86e000604b357", 48 | "5eb87d0affd86e000604b359", 49 | "5eb87d0fffd86e000604b35d", 50 | "5eb87d14ffd86e000604b361", 51 | "5eb87d16ffd86e000604b363", 52 | "5eb87d1affd86e000604b367", 53 | "5eb87d1fffd86e000604b36b", 54 | "5eb87d23ffd86e000604b36e", 55 | "5eb87d25ffd86e000604b370", 56 | "5eb87d28ffd86e000604b373", 57 | "5eb87d31ffd86e000604b379" 58 | ], 59 | "status": "active", 60 | "id": "5e9e4502f509092b78566f87" 61 | }, 62 | ... 63 | ], 64 | "totalDocs": 6, 65 | "offset": 0, 66 | "limit": 10, 67 | "totalPages": 1, 68 | "page": 1, 69 | "pagingCounter": 1, 70 | "hasPrevPage": false, 71 | "hasNextPage": false, 72 | "prevPage": null, 73 | "nextPage": null 74 | } 75 | ``` 76 | 77 | ## Error Responses 78 | 79 | **Code** : `400 Bad Request` 80 | 81 | **Content** : Mongoose error is shown, with suggestions to fix the query. 82 | -------------------------------------------------------------------------------- /docs/starlink/v4/all.md: -------------------------------------------------------------------------------- 1 | # Get all Starlink satellites 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/starlink` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "spaceTrack": { 17 | "CCSDS_OMM_VERS": "2.0", 18 | "COMMENT": "GENERATED VIA SPACE-TRACK.ORG API", 19 | "CREATION_DATE": "2020-06-19 21:46:09", 20 | "ORIGINATOR": "18 SPCS", 21 | "OBJECT_NAME": "STARLINK-1506", 22 | "OBJECT_ID": "2020-038T", 23 | "CENTER_NAME": "EARTH", 24 | "REF_FRAME": "TEME", 25 | "TIME_SYSTEM": "UTC", 26 | "MEAN_ELEMENT_THEORY": "SGP4", 27 | "EPOCH": "2020-06-19 20:00:01.000224", 28 | "MEAN_MOTION": 15.88829743, 29 | "ECCENTRICITY": 0.0087515, 30 | "INCLINATION": 53.002, 31 | "RA_OF_ASC_NODE": 266.3302, 32 | "ARG_OF_PERICENTER": 69.9474, 33 | "MEAN_ANOMALY": 221.4733, 34 | "EPHEMERIS_TYPE": 0, 35 | "CLASSIFICATION_TYPE": "U", 36 | "NORAD_CAT_ID": 45747, 37 | "ELEMENT_SET_NO": 999, 38 | "REV_AT_EPOCH": 212, 39 | "BSTAR": 0.01007, 40 | "MEAN_MOTION_DOT": 0.03503094, 41 | "MEAN_MOTION_DDOT": 0.01265, 42 | "SEMIMAJOR_AXIS": 6683.699, 43 | "PERIOD": 90.632, 44 | "APOAPSIS": 364.057, 45 | "PERIAPSIS": 247.072, 46 | "OBJECT_TYPE": "PAYLOAD", 47 | "RCS_SIZE": null, 48 | "COUNTRY_CODE": "US", 49 | "LAUNCH_DATE": "2020-06-13", 50 | "SITE": "AFETR", 51 | "DECAY_DATE": null, 52 | "DECAYED": 0, 53 | "FILE": 2768947, 54 | "GP_ID": 155985688, 55 | "TLE_LINE0": "0 STARLINK-1506", 56 | "TLE_LINE1": "1 45747U 20038T 20171.83334491 .03503094 12654-1 10068-1 0 9995", 57 | "TLE_LINE2": "2 45747 53.0017 266.3302 0087515 69.9474 221.4733 15.88829743 2124" 58 | }, 59 | "version": "v1.0", 60 | "launch": "5eb87d46ffd86e000604b389", 61 | "longitude": 165.93047730624068, 62 | "latitude": -52.91311434465077, 63 | "height_km": 446.61936740361125, 64 | "velocity_kms": 7.643507427834188, 65 | "id": "5eed7716096e590006985825" 66 | } 67 | ... 68 | ] 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/payloads/v4/query.md: -------------------------------------------------------------------------------- 1 | # Query payloads 2 | 3 | **Method** : `POST` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/payloads/query` 6 | 7 | **Auth required** : `False` 8 | 9 | **Body** : 10 | 11 | See [query](../../queries.md) guide for more details on building queries and paginating results. 12 | 13 | ```json 14 | { 15 | "query": {}, 16 | "options": {} 17 | } 18 | ``` 19 | 20 | ## Success Response 21 | 22 | **Code** : `200 OK` 23 | 24 | **Content example** : 25 | 26 | ```json 27 | { 28 | "docs": [ 29 | { 30 | "dragon": { 31 | "capsule": null, 32 | "mass_returned_kg": null, 33 | "mass_returned_lbs": null, 34 | "flight_time_sec": null, 35 | "manifest": null, 36 | "water_landing": null, 37 | "land_landing": null 38 | }, 39 | "name": "Tintin A & B", 40 | "type": "Satellite", 41 | "reused": false, 42 | "launch": "5eb87d14ffd86e000604b361", 43 | "customers": [ 44 | "SpaceX" 45 | ], 46 | "norad_ids": [ 47 | 43216, 48 | 43217 49 | ], 50 | "nationalities": [ 51 | "United States" 52 | ], 53 | "manufacturers": [ 54 | "SpaceX" 55 | ], 56 | "mass_kg": 800, 57 | "mass_lbs": 1763.7, 58 | "orbit": "SSO", 59 | "reference_system": "geocentric", 60 | "regime": "low-earth", 61 | "longitude": null, 62 | "semi_major_axis_km": 6737.42, 63 | "eccentricity": 0.0012995, 64 | "periapsis_km": 350.53, 65 | "apoapsis_km": 368.04, 66 | "inclination_deg": 97.4444, 67 | "period_min": 91.727, 68 | "lifespan_years": 1, 69 | "epoch": "2020-06-13T13:46:31.000Z", 70 | "mean_motion": 15.69864906, 71 | "raan": 176.6734, 72 | "arg_of_pericenter": 174.2326, 73 | "mean_anomaly": 185.9087, 74 | "id": "5eb0e4c6b6c3bb0006eeb21e" 75 | } 76 | ... 77 | ], 78 | "totalDocs": 136, 79 | "offset": 0, 80 | "limit": 10, 81 | "totalPages": 14, 82 | "page": 1, 83 | "pagingCounter": 1, 84 | "hasPrevPage": false, 85 | "hasNextPage": true, 86 | "prevPage": null, 87 | "nextPage": 2 88 | } 89 | ``` 90 | 91 | ## Error Responses 92 | 93 | **Code** : `400 Bad Request` 94 | 95 | **Content** : Mongoose error is shown, with suggestions to fix the query. 96 | -------------------------------------------------------------------------------- /docs/starlink/v4/one.md: -------------------------------------------------------------------------------- 1 | # Get one Starlink satellite 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/starlink/:id` 6 | 7 | **URL Parameters** : `id=[string]` where `id` is the ID of the Starlink sat 8 | 9 | **Auth required** : `False` 10 | 11 | ## Success Response 12 | 13 | **Code** : `200 OK` 14 | 15 | **Content example** : 16 | 17 | ```json 18 | { 19 | "spaceTrack": { 20 | "CCSDS_OMM_VERS": "2.0", 21 | "COMMENT": "GENERATED VIA SPACE-TRACK.ORG API", 22 | "CREATION_DATE": "2020-06-19 21:46:09", 23 | "ORIGINATOR": "18 SPCS", 24 | "OBJECT_NAME": "STARLINK-1506", 25 | "OBJECT_ID": "2020-038T", 26 | "CENTER_NAME": "EARTH", 27 | "REF_FRAME": "TEME", 28 | "TIME_SYSTEM": "UTC", 29 | "MEAN_ELEMENT_THEORY": "SGP4", 30 | "EPOCH": "2020-06-19 20:00:01.000224", 31 | "MEAN_MOTION": 15.88829743, 32 | "ECCENTRICITY": 0.0087515, 33 | "INCLINATION": 53.002, 34 | "RA_OF_ASC_NODE": 266.3302, 35 | "ARG_OF_PERICENTER": 69.9474, 36 | "MEAN_ANOMALY": 221.4733, 37 | "EPHEMERIS_TYPE": 0, 38 | "CLASSIFICATION_TYPE": "U", 39 | "NORAD_CAT_ID": 45747, 40 | "ELEMENT_SET_NO": 999, 41 | "REV_AT_EPOCH": 212, 42 | "BSTAR": 0.01007, 43 | "MEAN_MOTION_DOT": 0.03503094, 44 | "MEAN_MOTION_DDOT": 0.01265, 45 | "SEMIMAJOR_AXIS": 6683.699, 46 | "PERIOD": 90.632, 47 | "APOAPSIS": 364.057, 48 | "PERIAPSIS": 247.072, 49 | "OBJECT_TYPE": "PAYLOAD", 50 | "RCS_SIZE": null, 51 | "COUNTRY_CODE": "US", 52 | "LAUNCH_DATE": "2020-06-13", 53 | "SITE": "AFETR", 54 | "DECAY_DATE": null, 55 | "DECAYED": 0, 56 | "FILE": 2768947, 57 | "GP_ID": 155985688, 58 | "TLE_LINE0": "0 STARLINK-1506", 59 | "TLE_LINE1": "1 45747U 20038T 20171.83334491 .03503094 12654-1 10068-1 0 9995", 60 | "TLE_LINE2": "2 45747 53.0017 266.3302 0087515 69.9474 221.4733 15.88829743 2124" 61 | }, 62 | "version": "v1.0", 63 | "launch": "5eb87d46ffd86e000604b389", 64 | "longitude": 165.93047730624068, 65 | "latitude": -52.91311434465077, 66 | "height_km": 446.61936740361125, 67 | "velocity_kms": 7.643507427834188, 68 | "id": "5eed7716096e590006985825" 69 | } 70 | ``` 71 | 72 | ## Error Responses 73 | 74 | **Code** : `404 NOT FOUND` 75 | 76 | **Content** : `Not Found` 77 | -------------------------------------------------------------------------------- /jobs/landpads.js: -------------------------------------------------------------------------------- 1 | import got from 'got'; 2 | import { logger } from '../middleware/index.js'; 3 | 4 | const API = process.env.SPACEX_API; 5 | const KEY = process.env.SPACEX_KEY; 6 | const HEALTHCHECK = process.env.LANDPADS_HEALTHCHECK; 7 | 8 | /** 9 | * Update landpad attempts/successes 10 | * @return {Promise} 11 | */ 12 | export default async () => { 13 | try { 14 | const landpads = await got.post(`${API}/landpads/query`, { 15 | json: { 16 | options: { 17 | pagination: false, 18 | }, 19 | }, 20 | resolveBodyOnly: true, 21 | responseType: 'json', 22 | }); 23 | 24 | const updates = landpads.docs.map(async (landpad) => { 25 | const [attempts, successes] = await Promise.all([ 26 | got.post(`${API}/launches/query`, { 27 | json: { 28 | query: { 29 | cores: { 30 | $elemMatch: { 31 | landpad: landpad.id, 32 | landing_attempt: true, 33 | }, 34 | }, 35 | upcoming: false, 36 | success: true, 37 | }, 38 | options: { 39 | pagination: false, 40 | }, 41 | }, 42 | resolveBodyOnly: true, 43 | responseType: 'json', 44 | }), 45 | got.post(`${API}/launches/query`, { 46 | json: { 47 | query: { 48 | cores: { 49 | $elemMatch: { 50 | landpad: landpad.id, 51 | landing_attempt: true, 52 | landing_success: true, 53 | }, 54 | }, 55 | upcoming: false, 56 | success: true, 57 | }, 58 | options: { 59 | pagination: false, 60 | }, 61 | }, 62 | resolveBodyOnly: true, 63 | responseType: 'json', 64 | }), 65 | ]); 66 | 67 | await got.patch(`${API}/landpads/${landpad.id}`, { 68 | json: { 69 | landing_attempts: attempts.totalDocs, 70 | landing_successes: successes.totalDocs, 71 | }, 72 | headers: { 73 | 'spacex-key': KEY, 74 | }, 75 | }); 76 | }); 77 | 78 | await Promise.all(updates); 79 | 80 | logger.info('Landpads updated'); 81 | 82 | if (HEALTHCHECK) { 83 | await got(HEALTHCHECK); 84 | } 85 | } catch (error) { 86 | console.log(`Landpads Error: ${error.message}`); 87 | } 88 | }; 89 | -------------------------------------------------------------------------------- /middleware/cache.js: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis'; 2 | import blake3 from 'blake3'; 3 | import logger from './logger.js'; 4 | 5 | const redis = (process.env.SPACEX_REDIS) ? new Redis(process.env.SPACEX_REDIS) : new Redis(); 6 | let redisAvailable = false; 7 | 8 | redis.on('error', () => { 9 | redisAvailable = false; 10 | }); 11 | redis.on('end', () => { 12 | redisAvailable = false; 13 | }); 14 | redis.on('ready', () => { 15 | redisAvailable = true; 16 | logger.info('Redis connected'); 17 | }); 18 | 19 | /** 20 | * BLAKE3 hash func for redis keys 21 | * 22 | * @param {String} str String to hash 23 | * @returns {String} Hashed result 24 | */ 25 | const hash = (str) => blake3.createHash().update(str).digest('hex'); 26 | 27 | /** 28 | * Redis cache middleware 29 | * 30 | * @param {Number} ttl Cache TTL in seconds 31 | * @returns {void} 32 | */ 33 | export default (ttl) => async (ctx, next) => { 34 | if (process.env.NODE_ENV !== 'production') { 35 | await next(); 36 | return; 37 | } 38 | 39 | if (!redisAvailable) { 40 | ctx.response.set('spacex-api-cache-online', 'false'); 41 | await next(); 42 | return; 43 | } 44 | ctx.response.set('spacex-api-cache-online', 'true'); 45 | 46 | const { url, method } = ctx.request; 47 | const key = `spacex-cache:${hash(`${method}${url}${JSON.stringify(ctx.request.body)}`)}`; 48 | 49 | if (ttl) { 50 | ctx.response.set('Cache-Control', `max-age=${ttl}`); 51 | } else { 52 | ctx.response.set('Cache-Control', 'no-store'); 53 | } 54 | 55 | // Only allow cache on whitelist methods 56 | if (!['GET', 'POST'].includes(ctx.request.method)) { 57 | await next(); 58 | return; 59 | } 60 | 61 | let cached; 62 | try { 63 | cached = await redis.get(key); 64 | if (cached) { 65 | ctx.response.status = 200; 66 | ctx.response.set('spacex-api-cache', 'HIT'); 67 | ctx.response.type = 'application/json'; 68 | ctx.response.body = cached; 69 | cached = true; 70 | } 71 | } catch (e) { 72 | cached = false; 73 | } 74 | if (cached) { 75 | return; 76 | } 77 | await next(); 78 | 79 | const responseBody = JSON.stringify(ctx.response.body); 80 | ctx.response.set('spacex-api-cache', 'MISS'); 81 | 82 | // Set cache 83 | try { 84 | if ((ctx?.response?.status !== 200) || !responseBody) { 85 | return; 86 | } 87 | await redis.set(key, responseBody, 'EX', ttl); 88 | } catch (e) { 89 | console.log(`Failed to set cache: ${e.message}`); 90 | } 91 | }; 92 | 93 | export { 94 | redis, 95 | }; 96 | -------------------------------------------------------------------------------- /docs/landpads/v4/query.md: -------------------------------------------------------------------------------- 1 | # Query landing pads 2 | 3 | **Method** : `POST` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/landpads/query` 6 | 7 | **Auth required** : `False` 8 | 9 | **Body** : 10 | 11 | See [query](../../queries.md) guide for more details on building queries and paginating results. 12 | 13 | ```json 14 | { 15 | "query": {}, 16 | "options": {} 17 | } 18 | ``` 19 | 20 | ## Success Response 21 | 22 | **Code** : `200 OK` 23 | 24 | **Content example** : 25 | 26 | ```json 27 | { 28 | "docs": [ 29 | { 30 | "name": "LZ-1", 31 | "full_name": "Landing Zone 1", 32 | "status": "active", 33 | "type": "RTLS", 34 | "locality": "Cape Canaveral", 35 | "region": "Florida", 36 | "latitude": 28.485833, 37 | "longitude": -80.544444, 38 | "landing_attempts": 15, 39 | "landing_successes": 14, 40 | "wikipedia": "https://en.wikipedia.org/wiki/Landing_Zones_1_and_2", 41 | "details": "SpaceX's first east coast landing pad is Landing Zone 1, where the historic first Falcon 9 landing occurred in December 2015. LC-13 was originally used as a launch pad for early Atlas missiles and rockets from Lockheed Martin. LC-1 was later expanded to include Landing Zone 2 for side booster RTLS Falcon Heavy missions, and it was first used in February 2018 for that purpose.", 42 | "launches": [ 43 | "5eb87cefffd86e000604b342", 44 | "5eb87cf9ffd86e000604b349", 45 | "5eb87cfeffd86e000604b34d", 46 | "5eb87d01ffd86e000604b350", 47 | "5eb87d03ffd86e000604b352", 48 | "5eb87d07ffd86e000604b356", 49 | "5eb87d09ffd86e000604b358", 50 | "5eb87d0effd86e000604b35c", 51 | "5eb87d10ffd86e000604b35e", 52 | "5eb87d13ffd86e000604b360", 53 | "5eb87d26ffd86e000604b371", 54 | "5eb87d2dffd86e000604b376", 55 | "5eb87d35ffd86e000604b37a", 56 | "5eb87d36ffd86e000604b37b", 57 | "5eb87d42ffd86e000604b384" 58 | ], 59 | "id": "5e9e3032383ecb267a34e7c7" 60 | }, 61 | ... 62 | ], 63 | "totalDocs": 7, 64 | "offset": 0, 65 | "limit": 10, 66 | "totalPages": 1, 67 | "page": 1, 68 | "pagingCounter": 1, 69 | "hasPrevPage": false, 70 | "hasNextPage": false, 71 | "prevPage": null, 72 | "nextPage": null 73 | } 74 | ``` 75 | 76 | ## Error Responses 77 | 78 | **Code** : `400 Bad Request` 79 | 80 | **Content** : Mongoose error is shown, with suggestions to fix the query. 81 | -------------------------------------------------------------------------------- /docs/roadster/v4/query.md: -------------------------------------------------------------------------------- 1 | # Query roadster 2 | 3 | **Method** : `POST` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/roadster/query` 6 | 7 | **Auth required** : `False` 8 | 9 | **Body** : 10 | 11 | **NOTE:** Unlike other `/query` endpoints, this does not provide pagination, and only exposes the `select` ability in `options` to allow you to hide/show specific fields in the response. For more info on how select works, see the [mongoose](https://mongoosejs.com/docs/api.html#query_Query-select) docs. 12 | 13 | ```json 14 | { 15 | "query": {}, 16 | "options": { 17 | "select": { 18 | "norad_id": 1, 19 | } 20 | } 21 | } 22 | ``` 23 | 24 | ## Success Response 25 | 26 | **Code** : `200 OK` 27 | 28 | **Content example** : 29 | 30 | ```json 31 | { 32 | "flickr_images": [ 33 | "https://farm5.staticflickr.com/4615/40143096241_11128929df_b.jpg", 34 | "https://farm5.staticflickr.com/4702/40110298232_91b32d0cc0_b.jpg", 35 | "https://farm5.staticflickr.com/4676/40110297852_5e794b3258_b.jpg", 36 | "https://farm5.staticflickr.com/4745/40110304192_6e3e9a7a1b_b.jpg" 37 | ], 38 | "name": "Elon Musk's Tesla Roadster", 39 | "launch_date_utc": "2018-02-06T20:45:00.000Z", 40 | "launch_date_unix": 1517949900, 41 | "launch_mass_kg": 1350, 42 | "launch_mass_lbs": 2976, 43 | "norad_id": 43205, 44 | "epoch_jd": 2459014.345891204, 45 | "orbit_type": "heliocentric", 46 | "apoapsis_au": 1.663950009802517, 47 | "periapsis_au": 0.9859657216725529, 48 | "semi_major_axis_au": 196.2991348009594, 49 | "eccentricity": 0.2558512635239784, 50 | "inclination": 1.077499248052439, 51 | "longitude": 317.0839961949045, 52 | "periapsis_arg": 177.5240278992875, 53 | "period_days": 557.059427465354, 54 | "speed_kph": 72209.97792, 55 | "speed_mph": 44869.18619012833, 56 | "earth_distance_km": 220606726.83228922, 57 | "earth_distance_mi": 137078622.45850638, 58 | "mars_distance_km": 89348334.47067611, 59 | "mars_distance_mi": 55518463.93837848, 60 | "wikipedia": "https://en.wikipedia.org/wiki/Elon_Musk%27s_Tesla_Roadster", 61 | "video": "https://youtu.be/wbSwFU6tY1c", 62 | "details": "Elon Musk's Tesla Roadster is an electric sports car that served as the dummy payload for the February 2018 Falcon Heavy test flight and is now an artificial satellite of the Sun. Starman, a mannequin dressed in a spacesuit, occupies the driver's seat. The car and rocket are products of Tesla and SpaceX. This 2008-model Roadster was previously used by Musk for commuting, and is the only consumer car sent into space.", 63 | "id": "5eb75f0842fea42237d7f3f4" 64 | } 65 | ``` 66 | 67 | ## Error Responses 68 | 69 | **Code** : `400 Bad Request` 70 | 71 | **Content** : Mongoose error is shown, with suggestions to fix the query. 72 | -------------------------------------------------------------------------------- /jobs/launch-library.js: -------------------------------------------------------------------------------- 1 | import got from 'got'; 2 | import moment from 'moment-timezone'; 3 | import { fail, success } from '../lib/healthchecks/index.js'; 4 | import { logger } from '../middleware/index.js'; 5 | 6 | const { 7 | SPACEX_KEY, 8 | LAUNCH_LIBRARY_HEALTHCHECK, 9 | SPACEX_API: API, 10 | } = process.env; 11 | const LAUNCH_LIBRARY_API = 'https://ll.thespacedevs.com/2.2.0/launch/upcoming'; 12 | 13 | /** 14 | * Attach Launch Library v2 launch id's to upcoming launches 15 | * @return {Promise} 16 | */ 17 | export default async () => { 18 | try { 19 | const log = { 20 | name: 'launch-library', 21 | updated: false, 22 | }; 23 | const upcomingLaunches = await got.post(`${API}/launches/query`, { 24 | json: { 25 | query: { 26 | upcoming: true, 27 | }, 28 | options: { 29 | sort: { 30 | flight_number: 'asc', 31 | }, 32 | limit: 1, 33 | }, 34 | }, 35 | resolveBodyOnly: true, 36 | responseType: 'json', 37 | throwHttpErrors: false, 38 | }); 39 | if (upcomingLaunches.docs.length === 1) { 40 | const llLaunches = await got(LAUNCH_LIBRARY_API, { 41 | searchParams: { 42 | lsp__name: 'SpaceX', 43 | ordering: 'net', 44 | format: 'json', 45 | }, 46 | responseType: 'json', 47 | throwHttpErrors: false, 48 | }); 49 | if (llLaunches.statusCode === 200 && llLaunches.body.results.length) { 50 | const upcomingLaunch = upcomingLaunches.docs[0]; 51 | const dates = llLaunches.body.results.map((result) => ({ 52 | llDate: result.net, 53 | llId: result.id, 54 | })); 55 | const diffs = dates.map((date) => ({ 56 | diff: moment(upcomingLaunch.date_utc).diff(moment(date.llDate)), 57 | llId: date.llId, 58 | })); 59 | // Sort the date diffs by closeness to zero 60 | const close = diffs.reduce((a, b) => (Math.abs(b.diff - 0) < Math.abs(a.diff - 0) ? b : a)); 61 | await got.patch(`${API}/launches/${upcomingLaunch.id}`, { 62 | json: { 63 | launch_library_id: close.llId, 64 | }, 65 | headers: { 66 | 'spacex-key': SPACEX_KEY, 67 | }, 68 | }); 69 | log.spacexdataName = upcomingLaunch.name; 70 | log.launch_library_id = close.llId; 71 | log.updated = true; 72 | } 73 | } 74 | await success(LAUNCH_LIBRARY_HEALTHCHECK, log); 75 | logger.info(log); 76 | } catch (error) { 77 | const formatted = { 78 | name: 'launch-library', 79 | error: error.message, 80 | stack: error.stack, 81 | }; 82 | await fail(LAUNCH_LIBRARY_HEALTHCHECK, formatted); 83 | logger.error(formatted); 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /jobs/payloads.js: -------------------------------------------------------------------------------- 1 | import got from 'got'; 2 | import { CookieJar } from 'tough-cookie'; 3 | import { logger } from '../middleware/index.js'; 4 | 5 | const API = process.env.SPACEX_API; 6 | const KEY = process.env.SPACEX_KEY; 7 | const HEALTHCHECK = process.env.PAYLOADS_HEALTHCHECK; 8 | 9 | /** 10 | * Update payload orbit params 11 | * @return {Promise} 12 | */ 13 | export default async () => { 14 | try { 15 | const cookieJar = new CookieJar(); 16 | const [payloads] = await Promise.all([ 17 | got.post(`${API}/payloads/query`, { 18 | json: { 19 | query: {}, 20 | options: { 21 | pagination: false, 22 | }, 23 | }, 24 | resolveBodyOnly: true, 25 | responseType: 'json', 26 | }), 27 | got.post('https://www.space-track.org/ajaxauth/login', { 28 | form: { 29 | identity: process.env.SPACEX_TRACK_LOGIN, 30 | password: process.env.SPACEX_TRACK_PASSWORD, 31 | }, 32 | cookieJar, 33 | }), 34 | ]); 35 | 36 | const data = await got('https://www.space-track.org/basicspacedata/query/class/tle_latest/ORDINAL/1/orderby/NORAD_CAT_ID/epoch/>now-45/format/json', { 37 | resolveBodyOnly: true, 38 | responseType: 'json', 39 | cookieJar, 40 | }); 41 | 42 | const updates = payloads.docs.map(async (payload) => { 43 | const noradId = payload.norad_ids.shift() ?? null; 44 | const specificOrbit = data.find((sat) => parseInt(sat.NORAD_CAT_ID, 10) === noradId); 45 | if (specificOrbit) { 46 | await got.patch(`${API}/payloads/${payload.id}`, { 47 | json: { 48 | epoch: new Date(Date.parse(specificOrbit.EPOCH)).toISOString(), 49 | mean_motion: parseFloat(specificOrbit.MEAN_MOTION), 50 | raan: parseFloat(specificOrbit.RA_OF_ASC_NODE), 51 | arg_of_pericenter: parseFloat(specificOrbit.ARG_OF_PERICENTER), 52 | mean_anomaly: parseFloat(specificOrbit.MEAN_ANOMALY), 53 | semi_major_axis_km: parseFloat(specificOrbit.SEMIMAJOR_AXIS), 54 | eccentricity: parseFloat(specificOrbit.ECCENTRICITY), 55 | periapsis_km: parseFloat(specificOrbit.PERIGEE), 56 | apoapsis_km: parseFloat(specificOrbit.APOGEE), 57 | inclination_deg: parseFloat(specificOrbit.INCLINATION), 58 | period_min: parseFloat(specificOrbit.PERIOD), 59 | }, 60 | headers: { 61 | 'spacex-key': KEY, 62 | }, 63 | }); 64 | } 65 | }); 66 | 67 | await Promise.all(updates); 68 | 69 | logger.info({ 70 | orbitsUpdated: true, 71 | }); 72 | 73 | if (HEALTHCHECK) { 74 | await got(HEALTHCHECK); 75 | } 76 | } catch (error) { 77 | console.log(`Payloads Error: ${error.message}`); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /docs/payloads/v4/schema.md: -------------------------------------------------------------------------------- 1 | # Payload Schema 2 | 3 | ```json 4 | { 5 | "name": { 6 | "type": "String", 7 | "default": null, 8 | "unique": true 9 | }, 10 | "type": { 11 | "type": "String", 12 | "default": null 13 | }, 14 | "reused": { 15 | "type": "Boolean", 16 | "default": false 17 | }, 18 | "launch": { 19 | "type": "UUID", 20 | "default": null 21 | }, 22 | "customers": [ 23 | "String" 24 | ], 25 | "norad_ids": [ 26 | "Number" 27 | ], 28 | "nationalities": [ 29 | "String" 30 | ], 31 | "manufacturers": [ 32 | "String" 33 | ], 34 | "mass_kg": { 35 | "type": "Number", 36 | "default": null 37 | }, 38 | "mass_lbs": { 39 | "type": "Number", 40 | "default": null 41 | }, 42 | "orbit": { 43 | "type": "String", 44 | "default": null 45 | }, 46 | "reference_system": { 47 | "type": "String", 48 | "default": null 49 | }, 50 | "regime": { 51 | "type": "String", 52 | "default": null 53 | }, 54 | "longitude": { 55 | "type": "Number", 56 | "default": null 57 | }, 58 | "semi_major_axis_km": { 59 | "type": "Number", 60 | "default": null 61 | }, 62 | "eccentricity": { 63 | "type": "Number", 64 | "default": null 65 | }, 66 | "periapsis_km": { 67 | "type": "Number", 68 | "default": null 69 | }, 70 | "apoapsis_km": { 71 | "type": "Number", 72 | "default": null 73 | }, 74 | "inclination_deg": { 75 | "type": "Number", 76 | "default": null 77 | }, 78 | "period_min": { 79 | "type": "Number", 80 | "default": null 81 | }, 82 | "lifespan_years": { 83 | "type": "Number", 84 | "default": null 85 | }, 86 | "epoch": { 87 | "type": "String", 88 | "default": null 89 | }, 90 | "mean_motion": { 91 | "type": "Number", 92 | "default": null 93 | }, 94 | "raan": { 95 | "type": "Number", 96 | "default": null 97 | }, 98 | "arg_of_pericenter": { 99 | "type": "Number", 100 | "default": null 101 | }, 102 | "mean_anomaly": { 103 | "type": "Number", 104 | "default": null 105 | }, 106 | "dragon": { 107 | "capsule": { 108 | "type": "UUID", 109 | "default": null 110 | }, 111 | "mass_returned_kg": { 112 | "type": "Number", 113 | "default": null 114 | }, 115 | "mass_returned_lbs": { 116 | "type": "Number", 117 | "default": null 118 | }, 119 | "flight_time_sec": { 120 | "type": "Number", 121 | "default": null 122 | }, 123 | "manifest": { 124 | "type": "String", 125 | "default": null 126 | }, 127 | "water_landing": { 128 | "type": "Boolean", 129 | "default": null 130 | }, 131 | "land_landing": { 132 | "type": "Boolean", 133 | "default": null 134 | } 135 | } 136 | } 137 | ``` 138 | -------------------------------------------------------------------------------- /docs/dragons/v4/schema.md: -------------------------------------------------------------------------------- 1 | # Dragon Schema 2 | 3 | ```json 4 | { 5 | "name": { 6 | "type": "String", 7 | "unique": true, 8 | "required": true 9 | }, 10 | "type": { 11 | "type": "String", 12 | "required": true 13 | }, 14 | "active": { 15 | "type": "Boolean", 16 | "required": true 17 | }, 18 | "crew_capacity": { 19 | "type": "Number", 20 | "required": true 21 | }, 22 | "sidewall_angle_deg": { 23 | "type": "Number", 24 | "required": true 25 | }, 26 | "orbit_duration_yr": { 27 | "type": "Number", 28 | "required": true 29 | }, 30 | "dry_mass_kg": { 31 | "type": "Number", 32 | "required": true 33 | }, 34 | "dry_mass_lb": { 35 | "type": "Number", 36 | "required": true 37 | }, 38 | "first_flight": { 39 | "type": "String", 40 | "default": null 41 | }, 42 | "heat_shield": { 43 | "material": { 44 | "type": "String", 45 | "required": true 46 | }, 47 | "size_meters": { 48 | "type": "Number", 49 | "required": true 50 | }, 51 | "temp_degrees": { 52 | "type": "Number" 53 | }, 54 | "dev_partner": { 55 | "type": "String" 56 | } 57 | }, 58 | "thrusters": { 59 | "type": "Object" 60 | }, 61 | "launch_payload_mass": { 62 | "kg": { 63 | "type": "Number" 64 | }, 65 | "lb": { 66 | "type": "Number" 67 | } 68 | }, 69 | "launch_payload_vol": { 70 | "cubic_meters": { 71 | "type": "Number" 72 | }, 73 | "cubic_feet": { 74 | "type": "Number" 75 | } 76 | }, 77 | "return_payload_mass": { 78 | "kg": { 79 | "type": "Number" 80 | }, 81 | "lb": { 82 | "type": "Number" 83 | } 84 | }, 85 | "return_payload_vol": { 86 | "cubic_meters": { 87 | "type": "Number" 88 | }, 89 | "cubic_feet": { 90 | "type": "Number" 91 | } 92 | }, 93 | "pressurized_capsule": { 94 | "payload_volume": { 95 | "cubic_meters": { 96 | "type": "Number" 97 | }, 98 | "cubic_feet": { 99 | "type": "Number" 100 | } 101 | } 102 | }, 103 | "trunk": { 104 | "trunk_volume": { 105 | "cubic_meters": { 106 | "type": "Number" 107 | }, 108 | "cubic_feet": { 109 | "type": "Number" 110 | } 111 | }, 112 | "cargo": { 113 | "solar_array": { 114 | "type": "Number" 115 | }, 116 | "unpressurized_cargo": { 117 | "type": "Boolean" 118 | } 119 | } 120 | }, 121 | "height_w_trunk": { 122 | "meters": { 123 | "type": "Number" 124 | }, 125 | "feet": { 126 | "type": "Number" 127 | } 128 | }, 129 | "diameter": { 130 | "meters": { 131 | "type": "Number" 132 | }, 133 | "feet": { 134 | "type": "Number" 135 | } 136 | }, 137 | "flickr_images": { 138 | "type": [ 139 | "String" 140 | ] 141 | }, 142 | "wikipedia": { 143 | "type": "String" 144 | }, 145 | "description": { 146 | "type": "String" 147 | } 148 | } 149 | ``` 150 | -------------------------------------------------------------------------------- /models/payloads.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import mongoosePaginate from 'mongoose-paginate-v2'; 3 | import idPlugin from 'mongoose-id'; 4 | 5 | const payloadSchema = new mongoose.Schema({ 6 | name: { 7 | type: String, 8 | default: null, 9 | unique: true, 10 | }, 11 | type: { 12 | type: String, 13 | default: null, 14 | }, 15 | reused: { 16 | type: Boolean, 17 | default: false, 18 | }, 19 | launch: { 20 | type: mongoose.ObjectId, 21 | ref: 'Launch', 22 | default: null, 23 | }, 24 | customers: [String], 25 | norad_ids: [Number], 26 | nationalities: [String], 27 | manufacturers: [String], 28 | mass_kg: { 29 | type: Number, 30 | default: null, 31 | }, 32 | mass_lbs: { 33 | type: Number, 34 | default: null, 35 | }, 36 | orbit: { 37 | type: String, 38 | default: null, 39 | }, 40 | reference_system: { 41 | type: String, 42 | default: null, 43 | }, 44 | regime: { 45 | type: String, 46 | default: null, 47 | }, 48 | longitude: { 49 | type: Number, 50 | default: null, 51 | }, 52 | semi_major_axis_km: { 53 | type: Number, 54 | default: null, 55 | }, 56 | eccentricity: { 57 | type: Number, 58 | default: null, 59 | }, 60 | periapsis_km: { 61 | type: Number, 62 | default: null, 63 | }, 64 | apoapsis_km: { 65 | type: Number, 66 | default: null, 67 | }, 68 | inclination_deg: { 69 | type: Number, 70 | default: null, 71 | }, 72 | period_min: { 73 | type: Number, 74 | default: null, 75 | }, 76 | lifespan_years: { 77 | type: Number, 78 | default: null, 79 | }, 80 | epoch: { 81 | type: String, 82 | default: null, 83 | }, 84 | mean_motion: { 85 | type: Number, 86 | default: null, 87 | }, 88 | raan: { 89 | type: Number, 90 | default: null, 91 | }, 92 | arg_of_pericenter: { 93 | type: Number, 94 | default: null, 95 | }, 96 | mean_anomaly: { 97 | type: Number, 98 | default: null, 99 | }, 100 | dragon: { 101 | capsule: { 102 | type: mongoose.ObjectId, 103 | ref: 'Capsule', 104 | default: null, 105 | }, 106 | mass_returned_kg: { 107 | type: Number, 108 | default: null, 109 | }, 110 | mass_returned_lbs: { 111 | type: Number, 112 | default: null, 113 | }, 114 | flight_time_sec: { 115 | type: Number, 116 | default: null, 117 | }, 118 | manifest: { 119 | type: String, 120 | default: null, 121 | }, 122 | water_landing: { 123 | type: Boolean, 124 | default: null, 125 | }, 126 | land_landing: { 127 | type: Boolean, 128 | default: null, 129 | }, 130 | }, 131 | }, { autoCreate: true }); 132 | 133 | const index = { 134 | name: 'text', 135 | }; 136 | payloadSchema.index(index); 137 | 138 | payloadSchema.plugin(mongoosePaginate); 139 | payloadSchema.plugin(idPlugin); 140 | 141 | const Payload = mongoose.model('Payload', payloadSchema); 142 | 143 | export default Payload; 144 | -------------------------------------------------------------------------------- /docs/starlink/v4/query.md: -------------------------------------------------------------------------------- 1 | # Query Starlink satellites 2 | 3 | **Method** : `POST` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/starlink/query` 6 | 7 | **Auth required** : `False` 8 | 9 | **Body** : 10 | 11 | See [query](../../queries.md) guide for more details on building queries and paginating results. 12 | 13 | ```json 14 | { 15 | "query": {}, 16 | "options": {} 17 | } 18 | ``` 19 | 20 | ## Success Response 21 | 22 | **Code** : `200 OK` 23 | 24 | **Content example** : 25 | 26 | ```json 27 | { 28 | "docs": [ 29 | { 30 | "spaceTrack": { 31 | "CCSDS_OMM_VERS": "2.0", 32 | "COMMENT": "GENERATED VIA SPACE-TRACK.ORG API", 33 | "CREATION_DATE": "2020-06-19 21:36:08", 34 | "ORIGINATOR": "18 SPCS", 35 | "OBJECT_NAME": "STARLINK-30", 36 | "OBJECT_ID": "2019-029K", 37 | "CENTER_NAME": "EARTH", 38 | "REF_FRAME": "TEME", 39 | "TIME_SYSTEM": "UTC", 40 | "MEAN_ELEMENT_THEORY": "SGP4", 41 | "EPOCH": "2020-06-19 20:00:01.000224", 42 | "MEAN_MOTION": 15.43862877, 43 | "ECCENTRICITY": 0.000125, 44 | "INCLINATION": 52.996, 45 | "RA_OF_ASC_NODE": 195.8544, 46 | "ARG_OF_PERICENTER": 108.6906, 47 | "MEAN_ANOMALY": 109.3199, 48 | "EPHEMERIS_TYPE": 0, 49 | "CLASSIFICATION_TYPE": "U", 50 | "NORAD_CAT_ID": 44244, 51 | "ELEMENT_SET_NO": 999, 52 | "REV_AT_EPOCH": 5947, 53 | "BSTAR": 0.00007, 54 | "MEAN_MOTION_DOT": 0.00002829, 55 | "MEAN_MOTION_DDOT": 0, 56 | "SEMIMAJOR_AXIS": 6812.858, 57 | "PERIOD": 93.272, 58 | "APOAPSIS": 435.574, 59 | "PERIAPSIS": 433.871, 60 | "OBJECT_TYPE": "PAYLOAD", 61 | "RCS_SIZE": "LARGE", 62 | "COUNTRY_CODE": "US", 63 | "LAUNCH_DATE": "2019-05-24", 64 | "SITE": "AFETR", 65 | "DECAY_DATE": null, 66 | "DECAYED": 0, 67 | "FILE": 2768931, 68 | "GP_ID": 155985469, 69 | "TLE_LINE0": "0 STARLINK-30", 70 | "TLE_LINE1": "1 44244U 19029K 20171.83334491 .00002829 00000-0 70479-4 0 9997", 71 | "TLE_LINE2": "2 44244 52.9964 195.8544 0001250 108.6906 109.3199 15.43862877 59477" 72 | }, 73 | "version": "v0.9", 74 | "launch": "5eb87d30ffd86e000604b378", 75 | "longitude": 10.551678198548517, 76 | "latitude": 8.26018124742001, 77 | "height_km": 434.5577668080887, 78 | "velocity_kms": 7.653046786650296, 79 | "id": "5eed770f096e59000698560d" 80 | }, 81 | ... 82 | ], 83 | "totalDocs": 537, 84 | "offset": 0, 85 | "limit": 10, 86 | "totalPages": 54, 87 | "page": 1, 88 | "pagingCounter": 1, 89 | "hasPrevPage": false, 90 | "hasNextPage": true, 91 | "prevPage": null, 92 | "nextPage": 2 93 | } 94 | ``` 95 | 96 | ## Error Responses 97 | 98 | **Code** : `400 Bad Request` 99 | 100 | **Content** : Mongoose error is shown, with suggestions to fix the query. 101 | -------------------------------------------------------------------------------- /docs/dragons/v4/all.md: -------------------------------------------------------------------------------- 1 | # Get all Dragons 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/dragons` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "heat_shield": { 17 | "material": "PICA-X", 18 | "size_meters": 3.6, 19 | "temp_degrees": 3000, 20 | "dev_partner": "NASA" 21 | }, 22 | "launch_payload_mass": { 23 | "kg": 6000, 24 | "lb": 13228 25 | }, 26 | "launch_payload_vol": { 27 | "cubic_meters": 25, 28 | "cubic_feet": 883 29 | }, 30 | "return_payload_mass": { 31 | "kg": 3000, 32 | "lb": 6614 33 | }, 34 | "return_payload_vol": { 35 | "cubic_meters": 11, 36 | "cubic_feet": 388 37 | }, 38 | "pressurized_capsule": { 39 | "payload_volume": { 40 | "cubic_meters": 11, 41 | "cubic_feet": 388 42 | } 43 | }, 44 | "trunk": { 45 | "trunk_volume": { 46 | "cubic_meters": 14, 47 | "cubic_feet": 494 48 | }, 49 | "cargo": { 50 | "solar_array": 2, 51 | "unpressurized_cargo": true 52 | } 53 | }, 54 | "height_w_trunk": { 55 | "meters": 7.2, 56 | "feet": 23.6 57 | }, 58 | "diameter": { 59 | "meters": 3.7, 60 | "feet": 12 61 | }, 62 | "first_flight": "2010-12-08", 63 | "flickr_images": [ 64 | "https://www.spacex.com/sites/spacex/files/styles/media_gallery_large/public/2015_-_04_crs5_dragon_orbit13.jpg?itok=9p8_l7UP", 65 | "https://www.spacex.com/sites/spacex/files/styles/media_gallery_large/public/2012_-_4_dragon_grapple_cots2-1.jpg?itok=R2-SeuMX", 66 | "https://farm3.staticflickr.com/2815/32761844973_4b55b27d3c_b.jpg", 67 | "https://farm9.staticflickr.com/8618/16649075267_d18cbb4342_b.jpg" 68 | ], 69 | "name": "Dragon 1", 70 | "type": "capsule", 71 | "active": true, 72 | "crew_capacity": 0, 73 | "sidewall_angle_deg": 15, 74 | "orbit_duration_yr": 2, 75 | "dry_mass_kg": 4200, 76 | "dry_mass_lb": 9300, 77 | "thrusters": [ 78 | { 79 | "type": "Draco", 80 | "amount": 18, 81 | "pods": 4, 82 | "fuel_1": "nitrogen tetroxide", 83 | "fuel_2": "monomethylhydrazine", 84 | "isp": 300, 85 | "thrust": { 86 | "kN": 0.4, 87 | "lbf": 90 88 | } 89 | } 90 | ], 91 | "wikipedia": "https://en.wikipedia.org/wiki/SpaceX_Dragon", 92 | "description": "Dragon is a reusable spacecraft developed by SpaceX, an American private space transportation company based in Hawthorne, California. Dragon is launched into space by the SpaceX Falcon 9 two-stage-to-orbit launch vehicle. The Dragon spacecraft was originally designed for human travel, but so far has only been used to deliver cargo to the International Space Station (ISS).", 93 | "id": "5e9d058759b1ff74a7ad5f8f" 94 | }, 95 | ... 96 | ] 97 | ``` 98 | -------------------------------------------------------------------------------- /jobs/webcast.js: -------------------------------------------------------------------------------- 1 | import got from 'got'; 2 | import * as fuzz from 'fuzzball'; 3 | import Parser from 'rss-parser'; 4 | import { fail, success } from '../lib/healthchecks/index.js'; 5 | import { logger } from '../middleware/index.js'; 6 | 7 | const YOUTUBE_PREFIX = 'https://youtu.be'; 8 | const CHANNEL_ID = 'UCtI0Hodo5o5dUb67FeUjDeA'; 9 | const { 10 | SPACEX_KEY, 11 | WEBCAST_HEALTHCHECK, 12 | SPACEX_API: API, 13 | } = process.env; 14 | 15 | /** 16 | * Check for new SpaceX webcast links 17 | * @return {Promise} 18 | */ 19 | export default async () => { 20 | try { 21 | let updated = false; 22 | let match = false; 23 | const parser = new Parser(); 24 | const url = `https://www.youtube.com/feeds/videos.xml?channel_id=${CHANNEL_ID}`; 25 | const rssResult = await got(url, { 26 | resolveBodyOnly: true, 27 | }); 28 | const result = await parser.parseString(rssResult); 29 | const latest = result.items[0]; 30 | const rssTitle = latest.title; 31 | const rssYoutubeId = latest.link.split('v=')[1]; 32 | 33 | const launches = await got.post(`${API}/launches/query`, { 34 | json: { 35 | query: { 36 | upcoming: true, 37 | }, 38 | options: { 39 | sort: { 40 | flight_number: 'asc', 41 | }, 42 | limit: 1, 43 | }, 44 | }, 45 | resolveBodyOnly: true, 46 | responseType: 'json', 47 | }); 48 | const launchId = launches.docs[0].id; 49 | const missionName = launches.docs[0].name; 50 | 51 | const ratio = fuzz.ratio(rssTitle, missionName); 52 | // Prevent matching on mission control audio videos 53 | if (!rssTitle.includes('audio') && ratio >= 50) { 54 | match = true; 55 | const pastLaunches = await got.post(`${API}/launches/query`, { 56 | json: { 57 | query: { 58 | upcoming: false, 59 | }, 60 | options: { 61 | sort: { 62 | flight_number: 'desc', 63 | }, 64 | limit: 1, 65 | }, 66 | }, 67 | resolveBodyOnly: true, 68 | responseType: 'json', 69 | }); 70 | const pastYoutubeId = pastLaunches.docs[0].links.youtube_id; 71 | if (rssYoutubeId !== pastYoutubeId) { 72 | await got.patch(`${API}/launches/${launchId}`, { 73 | json: { 74 | 'links.webcast': `${YOUTUBE_PREFIX}/${rssYoutubeId}`, 75 | 'links.youtube_id': rssYoutubeId, 76 | }, 77 | headers: { 78 | 'spacex-key': SPACEX_KEY, 79 | }, 80 | }); 81 | updated = true; 82 | } 83 | } 84 | const log = { 85 | name: 'webcast', 86 | ratio, 87 | match, 88 | updated, 89 | youtubeTitle: rssTitle, 90 | youtubeId: rssYoutubeId, 91 | }; 92 | await success(WEBCAST_HEALTHCHECK, log); 93 | logger.info(log); 94 | } catch (error) { 95 | const formatted = { 96 | name: 'webcast', 97 | error: error.message, 98 | stack: error.stack, 99 | }; 100 | await fail(WEBCAST_HEALTHCHECK, formatted); 101 | logger.error(formatted); 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /models/dragons.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import mongoosePaginate from 'mongoose-paginate-v2'; 3 | import idPlugin from 'mongoose-id'; 4 | 5 | const dragonSchema = new mongoose.Schema({ 6 | name: { 7 | type: String, 8 | unique: true, 9 | required: true, 10 | }, 11 | type: { 12 | type: String, 13 | required: true, 14 | }, 15 | active: { 16 | type: Boolean, 17 | required: true, 18 | }, 19 | crew_capacity: { 20 | type: Number, 21 | required: true, 22 | }, 23 | sidewall_angle_deg: { 24 | type: Number, 25 | required: true, 26 | }, 27 | orbit_duration_yr: { 28 | type: Number, 29 | required: true, 30 | }, 31 | dry_mass_kg: { 32 | type: Number, 33 | required: true, 34 | }, 35 | dry_mass_lb: { 36 | type: Number, 37 | required: true, 38 | }, 39 | first_flight: { 40 | type: String, 41 | default: null, 42 | }, 43 | heat_shield: { 44 | material: { 45 | type: String, 46 | required: true, 47 | }, 48 | size_meters: { 49 | type: Number, 50 | required: true, 51 | }, 52 | temp_degrees: { 53 | type: Number, 54 | }, 55 | dev_partner: { 56 | type: String, 57 | }, 58 | }, 59 | thrusters: { 60 | type: mongoose.Mixed, 61 | }, 62 | launch_payload_mass: { 63 | kg: { 64 | type: Number, 65 | }, 66 | lb: { 67 | type: Number, 68 | }, 69 | }, 70 | launch_payload_vol: { 71 | cubic_meters: { 72 | type: Number, 73 | }, 74 | cubic_feet: { 75 | type: Number, 76 | }, 77 | }, 78 | return_payload_mass: { 79 | kg: { 80 | type: Number, 81 | }, 82 | lb: { 83 | type: Number, 84 | }, 85 | }, 86 | return_payload_vol: { 87 | cubic_meters: { 88 | type: Number, 89 | }, 90 | cubic_feet: { 91 | type: Number, 92 | }, 93 | }, 94 | pressurized_capsule: { 95 | payload_volume: { 96 | cubic_meters: { 97 | type: Number, 98 | }, 99 | cubic_feet: { 100 | type: Number, 101 | }, 102 | }, 103 | }, 104 | trunk: { 105 | trunk_volume: { 106 | cubic_meters: { 107 | type: Number, 108 | }, 109 | cubic_feet: { 110 | type: Number, 111 | }, 112 | }, 113 | cargo: { 114 | solar_array: { 115 | type: Number, 116 | }, 117 | unpressurized_cargo: { 118 | type: Boolean, 119 | }, 120 | }, 121 | }, 122 | height_w_trunk: { 123 | meters: { 124 | type: Number, 125 | }, 126 | feet: { 127 | type: Number, 128 | }, 129 | }, 130 | diameter: { 131 | meters: { 132 | type: Number, 133 | }, 134 | feet: { 135 | type: Number, 136 | }, 137 | }, 138 | flickr_images: { 139 | type: [ 140 | String, 141 | ], 142 | }, 143 | wikipedia: { 144 | type: String, 145 | }, 146 | description: { 147 | type: String, 148 | }, 149 | }, { autoCreate: true }); 150 | 151 | const index = { 152 | name: 'text', 153 | }; 154 | dragonSchema.index(index); 155 | 156 | dragonSchema.plugin(mongoosePaginate); 157 | dragonSchema.plugin(idPlugin); 158 | 159 | const Dragon = mongoose.model('Dragon', dragonSchema); 160 | 161 | export default Dragon; 162 | -------------------------------------------------------------------------------- /docs/dragons/v4/one.md: -------------------------------------------------------------------------------- 1 | # Get one Dragon 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/dragons/:id` 6 | 7 | **URL Parameters** : `id=[string]` where `id` is the ID of the dragon 8 | 9 | **Auth required** : `False` 10 | 11 | ## Success Response 12 | 13 | **Code** : `200 OK` 14 | 15 | **Content example** : 16 | 17 | ```json 18 | { 19 | "heat_shield": { 20 | "material": "PICA-X", 21 | "size_meters": 3.6, 22 | "temp_degrees": 3000, 23 | "dev_partner": "NASA" 24 | }, 25 | "launch_payload_mass": { 26 | "kg": 6000, 27 | "lb": 13228 28 | }, 29 | "launch_payload_vol": { 30 | "cubic_meters": 25, 31 | "cubic_feet": 883 32 | }, 33 | "return_payload_mass": { 34 | "kg": 3000, 35 | "lb": 6614 36 | }, 37 | "return_payload_vol": { 38 | "cubic_meters": 11, 39 | "cubic_feet": 388 40 | }, 41 | "pressurized_capsule": { 42 | "payload_volume": { 43 | "cubic_meters": 11, 44 | "cubic_feet": 388 45 | } 46 | }, 47 | "trunk": { 48 | "trunk_volume": { 49 | "cubic_meters": 14, 50 | "cubic_feet": 494 51 | }, 52 | "cargo": { 53 | "solar_array": 2, 54 | "unpressurized_cargo": true 55 | } 56 | }, 57 | "height_w_trunk": { 58 | "meters": 7.2, 59 | "feet": 23.6 60 | }, 61 | "diameter": { 62 | "meters": 3.7, 63 | "feet": 12 64 | }, 65 | "first_flight": "2010-12-08", 66 | "flickr_images": [ 67 | "https://www.spacex.com/sites/spacex/files/styles/media_gallery_large/public/2015_-_04_crs5_dragon_orbit13.jpg?itok=9p8_l7UP", 68 | "https://www.spacex.com/sites/spacex/files/styles/media_gallery_large/public/2012_-_4_dragon_grapple_cots2-1.jpg?itok=R2-SeuMX", 69 | "https://farm3.staticflickr.com/2815/32761844973_4b55b27d3c_b.jpg", 70 | "https://farm9.staticflickr.com/8618/16649075267_d18cbb4342_b.jpg" 71 | ], 72 | "name": "Dragon 1", 73 | "type": "capsule", 74 | "active": true, 75 | "crew_capacity": 0, 76 | "sidewall_angle_deg": 15, 77 | "orbit_duration_yr": 2, 78 | "dry_mass_kg": 4200, 79 | "dry_mass_lb": 9300, 80 | "thrusters": [ 81 | { 82 | "type": "Draco", 83 | "amount": 18, 84 | "pods": 4, 85 | "fuel_1": "nitrogen tetroxide", 86 | "fuel_2": "monomethylhydrazine", 87 | "isp": 300, 88 | "thrust": { 89 | "kN": 0.4, 90 | "lbf": 90 91 | } 92 | } 93 | ], 94 | "wikipedia": "https://en.wikipedia.org/wiki/SpaceX_Dragon", 95 | "description": "Dragon is a reusable spacecraft developed by SpaceX, an American private space transportation company based in Hawthorne, California. Dragon is launched into space by the SpaceX Falcon 9 two-stage-to-orbit launch vehicle. The Dragon spacecraft was originally designed for human travel, but so far has only been used to deliver cargo to the International Space Station (ISS).", 96 | "id": "5e9d058759b1ff74a7ad5f8f" 97 | } 98 | ``` 99 | 100 | ## Error Responses 101 | 102 | **Code** : `404 NOT FOUND` 103 | 104 | **Content** : `Not Found` 105 | -------------------------------------------------------------------------------- /docs/clients.md: -------------------------------------------------------------------------------- 1 | # List of known API clients / wrappers 2 | 3 | > _Do you, or do you know of some client/wrapper, that makes use of this community maintained service? If so, please [create an issue](https://github.com/r-spacex/SpaceX-API/issues/new) or [submit a PR](https://github.com/r-spacex/SpaceX-API/pulls) with additions to this list. Thanks_ 4 | 5 | NOTE: Clients are grouped by API version(s) supported 6 | 7 | ### V4 (Latest) 8 | 9 | |Name|Lang|Creator(s)|Repo| 10 | |:---|:---|:---|:---| 11 | | Oddity | .NET | Tearth | [GitHub](https://github.com/Tearth/Oddity) | 12 | | SpaceXPy | Python | Ryu JuHeon | [GitHub](https://github.com/SaidBySolo/SpaceXPy) | 13 | | KSBSpacexKit | Swift | SaiBalaji K| [GitHub](https://github.com/SaiBalaji22/KSBSpacexKit) | 14 | | Marsy | C++ | AzuxDario | [GitHub](https://github.com/AzuxDario/Marsy) | 15 | | Spacex-api.js | Node.js | AkiaCode | [Github](https://github.com/AkiaCode/spacex-api.js) | 16 | | spacex_api | Dart | Ahsanz024 | [Github](https://github.com/ahsanz024/spacex_api) | 17 | | spacex_api | Ruby | Victor Perez | [Github](https://github.com/victorperez/spacex-api-ruby) | 18 | | xploration-graphql | TypeScript | Kartik Kumar Gujarati | [Github](https://github.com/Kartikkumargujarati/xploration-graphql) | 19 | | spacex-graphql-gateway | TypeScript | Kevin Hermawan | [Github](https://github.com/kevinstd/spacex-graphql-gateway) | 20 | | r/SpaceX (Independent Publisher) | [Power Platform](https://docs.microsoft.com/en-us/connectors/rspacexip/) | Troy Taylor | [Github](https://github.com/troystaylor/PowerPlatformConnectors/tree/r/SpaceX/independent-publisher-connectors/rSpaceX) | 21 | | spacex-api | Java | Andrey Vasilyev | [Github](https://github.com/artfultom/spacex-api) | 22 | 23 | ### V3, V2, V1 (Deprecated) 24 | 25 | |Name|Lang|Creator(s)|Repo| 26 | |:---|:---|:---|:---| 27 | | SpaceX-GraphQL | TypeScript | Jordan Owens | [GitHub](https://github.com/jor-dan/SpaceX-GraphQL) | 28 | | spacex-graphql-api | GraphQL | Emerson Laurentino | [GitHub](https://github.com/emersonlaurentino/spacex-qraphql-api) | 29 | | SpaceX-API-Wrapper | Node.js | Thomas Smyth | [GitHub](https://github.com/Thomas-Smyth/SpaceX-API-Wrapper) | 30 | | spacex-api | TypeScript / Node.js | Tomasz Borowski | [GitHub](https://github.com/tbprojects/spacex-api), [NPM](https://www.npmjs.com/package/spacex-api) | 31 | | SpaceX | PowerShell | François-Xavier Cat | [GitHub](https://github.com/lazywinadmin/SpaceX) | 32 | | SpacePY-X | Python | Andrew Shapton | [GitHub](https://github.com/alshapton/SpacePY-X) | 33 | | SpaceX-PY | Python | Kaylum Lally | [GitHub](https://github.com/HiKaylum/SpaceX-PY) | 34 | | SpaceNIM-X | Nim | Andrew Shapton | [GitHub](https://github.com/alshapton/SpaceNIM-X) | 35 | | SpaceCRYST-X | Crystal | Andrew Shapton | [GitHub](https://github.com/alshapton/SpaceCRYST-X) | 36 | | spacex | Go | Or Hiltch | [GitHub](https://github.com/orcaman/spacex) | 37 | | spacex-api-wrapper | Rust | Alex Gutan | [GitHub](https://github.com/AGutan/spacex-api-wrapper)| 38 | | spacex-graphql-rust | Rust | Aaron Feigenbaum | [GitHub](https://github.com/adace123/spacex-graphql-rust)| 39 | | space-rx | Rust | Tyler Wilcock | [GitHub](https://github.com/twilco/space-rx) | 40 | | spacex | Ruby | Rodolfo | [GitHub](https://github.com/rodolfobandeira/spacex) | 41 | | spacex | PHP | Aires Gonçalves | [GitHub](https://github.com/airesvsg/spacex) | 42 | | spacex_ex | Elixir | Chen Zhao | [GitHub](https://github.com/crunchysoul/spacex_ex) | 43 | | SpaceX | R | Johannes Friedrich | [GitHub](https://github.com/JohannesFriedrich/SpaceX) | 44 | | SpaceXAPI-Swift | Swift | Sami Sharafeddine | [GitHub](https://github.com/devsamsh/SpaceXAPI-Swift) | 45 | -------------------------------------------------------------------------------- /docs/launches/v4/next.md: -------------------------------------------------------------------------------- 1 | # Get next launch 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/launches/next` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | { 15 | "fairings": null, 16 | "links": { 17 | "patch": { 18 | "small": "https://images2.imgbox.com/53/22/dh0XSLXO_o.png", 19 | "large": "https://images2.imgbox.com/15/2b/NAcsTEB6_o.png" 20 | }, 21 | "reddit": { 22 | "campaign": "https://www.reddit.com/r/spacex/comments/ezn6n0/crs20_launch_campaign_thread", 23 | "launch": "https://www.reddit.com/r/spacex/comments/fe8pcj/rspacex_crs20_official_launch_discussion_updates/", 24 | "media": "https://www.reddit.com/r/spacex/comments/fes64p/rspacex_crs20_media_thread_videos_images_gifs/", 25 | "recovery": null 26 | }, 27 | "flickr": { 28 | "small": [], 29 | "original": [ 30 | "https://live.staticflickr.com/65535/49635401403_96f9c322dc_o.jpg", 31 | "https://live.staticflickr.com/65535/49636202657_e81210a3ca_o.jpg", 32 | "https://live.staticflickr.com/65535/49636202572_8831c5a917_o.jpg", 33 | "https://live.staticflickr.com/65535/49635401423_e0bef3e82f_o.jpg", 34 | "https://live.staticflickr.com/65535/49635985086_660be7062f_o.jpg" 35 | ] 36 | }, 37 | "presskit": "https://www.spacex.com/sites/spacex/files/crs-20_mission_press_kit.pdf", 38 | "webcast": "https://youtu.be/1MkcWK2PnsU", 39 | "youtube_id": "1MkcWK2PnsU", 40 | "article": "https://spaceflightnow.com/2020/03/07/late-night-launch-of-spacex-cargo-ship-marks-end-of-an-era/", 41 | "wikipedia": "https://en.wikipedia.org/wiki/SpaceX_CRS-20" 42 | }, 43 | "static_fire_date_utc": "2020-03-01T10:20:00.000Z", 44 | "static_fire_date_unix": 1583058000, 45 | "tdb": false, 46 | "net": false, 47 | "window": 0, 48 | "rocket": "5e9d0d95eda69973a809d1ec", 49 | "success": true, 50 | "failures": [], 51 | "details": "SpaceX's 20th and final Crew Resupply Mission under the original NASA CRS contract, this mission brings essential supplies to the International Space Station using SpaceX's reusable Dragon spacecraft. It is the last scheduled flight of a Dragon 1 capsule. (CRS-21 and up under the new Commercial Resupply Services 2 contract will use Dragon 2.) The external payload for this mission is the Bartolomeo ISS external payload hosting platform. Falcon 9 and Dragon will launch from SLC-40, Cape Canaveral Air Force Station and the booster will land at LZ-1. The mission will be complete with return and recovery of the Dragon capsule and down cargo.", 52 | "crew": [], 53 | "ships": [], 54 | "capsules": [ 55 | "5e9e2c5cf359185d753b266f" 56 | ], 57 | "payloads": [ 58 | "5eb0e4d0b6c3bb0006eeb253" 59 | ], 60 | "launchpad": "5e9e4501f509094ba4566f84", 61 | "auto_update": true, 62 | "flight_number": 91, 63 | "name": "CRS-20", 64 | "date_utc": "2020-03-07T04:50:31.000Z", 65 | "date_unix": 1583556631, 66 | "date_local": "2020-03-06T23:50:31-05:00", 67 | "date_precision": "hour", 68 | "upcoming": false, 69 | "cores": [ 70 | { 71 | "core": "5e9e28a7f359187afd3b2662", 72 | "flight": 2, 73 | "gridfins": true, 74 | "legs": true, 75 | "reused": true, 76 | "landing_attempt": true, 77 | "landing_success": true, 78 | "landing_type": "RTLS", 79 | "landpad": "5e9e3032383ecb267a34e7c7" 80 | } 81 | ], 82 | "id": "5eb87d42ffd86e000604b384" 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/launches/v5/next.md: -------------------------------------------------------------------------------- 1 | # Get next launch 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v5/launches/next` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | { 15 | "fairings": null, 16 | "links": { 17 | "patch": { 18 | "small": "https://images2.imgbox.com/53/22/dh0XSLXO_o.png", 19 | "large": "https://images2.imgbox.com/15/2b/NAcsTEB6_o.png" 20 | }, 21 | "reddit": { 22 | "campaign": "https://www.reddit.com/r/spacex/comments/ezn6n0/crs20_launch_campaign_thread", 23 | "launch": "https://www.reddit.com/r/spacex/comments/fe8pcj/rspacex_crs20_official_launch_discussion_updates/", 24 | "media": "https://www.reddit.com/r/spacex/comments/fes64p/rspacex_crs20_media_thread_videos_images_gifs/", 25 | "recovery": null 26 | }, 27 | "flickr": { 28 | "small": [], 29 | "original": [ 30 | "https://live.staticflickr.com/65535/49635401403_96f9c322dc_o.jpg", 31 | "https://live.staticflickr.com/65535/49636202657_e81210a3ca_o.jpg", 32 | "https://live.staticflickr.com/65535/49636202572_8831c5a917_o.jpg", 33 | "https://live.staticflickr.com/65535/49635401423_e0bef3e82f_o.jpg", 34 | "https://live.staticflickr.com/65535/49635985086_660be7062f_o.jpg" 35 | ] 36 | }, 37 | "presskit": "https://www.spacex.com/sites/spacex/files/crs-20_mission_press_kit.pdf", 38 | "webcast": "https://youtu.be/1MkcWK2PnsU", 39 | "youtube_id": "1MkcWK2PnsU", 40 | "article": "https://spaceflightnow.com/2020/03/07/late-night-launch-of-spacex-cargo-ship-marks-end-of-an-era/", 41 | "wikipedia": "https://en.wikipedia.org/wiki/SpaceX_CRS-20" 42 | }, 43 | "static_fire_date_utc": "2020-03-01T10:20:00.000Z", 44 | "static_fire_date_unix": 1583058000, 45 | "tdb": false, 46 | "net": false, 47 | "window": 0, 48 | "rocket": "5e9d0d95eda69973a809d1ec", 49 | "success": true, 50 | "failures": [], 51 | "details": "SpaceX's 20th and final Crew Resupply Mission under the original NASA CRS contract, this mission brings essential supplies to the International Space Station using SpaceX's reusable Dragon spacecraft. It is the last scheduled flight of a Dragon 1 capsule. (CRS-21 and up under the new Commercial Resupply Services 2 contract will use Dragon 2.) The external payload for this mission is the Bartolomeo ISS external payload hosting platform. Falcon 9 and Dragon will launch from SLC-40, Cape Canaveral Air Force Station and the booster will land at LZ-1. The mission will be complete with return and recovery of the Dragon capsule and down cargo.", 52 | "crew": [], 53 | "ships": [], 54 | "capsules": [ 55 | "5e9e2c5cf359185d753b266f" 56 | ], 57 | "payloads": [ 58 | "5eb0e4d0b6c3bb0006eeb253" 59 | ], 60 | "launchpad": "5e9e4501f509094ba4566f84", 61 | "auto_update": true, 62 | "flight_number": 91, 63 | "name": "CRS-20", 64 | "date_utc": "2020-03-07T04:50:31.000Z", 65 | "date_unix": 1583556631, 66 | "date_local": "2020-03-06T23:50:31-05:00", 67 | "date_precision": "hour", 68 | "upcoming": false, 69 | "cores": [ 70 | { 71 | "core": "5e9e28a7f359187afd3b2662", 72 | "flight": 2, 73 | "gridfins": true, 74 | "legs": true, 75 | "reused": true, 76 | "landing_attempt": true, 77 | "landing_success": true, 78 | "landing_type": "RTLS", 79 | "landpad": "5e9e3032383ecb267a34e7c7" 80 | } 81 | ], 82 | "id": "5eb87d42ffd86e000604b384" 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/launches/v4/latest.md: -------------------------------------------------------------------------------- 1 | # Get latest launch 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/launches/latest` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | { 15 | "fairings": null, 16 | "links": { 17 | "patch": { 18 | "small": "https://images2.imgbox.com/53/22/dh0XSLXO_o.png", 19 | "large": "https://images2.imgbox.com/15/2b/NAcsTEB6_o.png" 20 | }, 21 | "reddit": { 22 | "campaign": "https://www.reddit.com/r/spacex/comments/ezn6n0/crs20_launch_campaign_thread", 23 | "launch": "https://www.reddit.com/r/spacex/comments/fe8pcj/rspacex_crs20_official_launch_discussion_updates/", 24 | "media": "https://www.reddit.com/r/spacex/comments/fes64p/rspacex_crs20_media_thread_videos_images_gifs/", 25 | "recovery": null 26 | }, 27 | "flickr": { 28 | "small": [], 29 | "original": [ 30 | "https://live.staticflickr.com/65535/49635401403_96f9c322dc_o.jpg", 31 | "https://live.staticflickr.com/65535/49636202657_e81210a3ca_o.jpg", 32 | "https://live.staticflickr.com/65535/49636202572_8831c5a917_o.jpg", 33 | "https://live.staticflickr.com/65535/49635401423_e0bef3e82f_o.jpg", 34 | "https://live.staticflickr.com/65535/49635985086_660be7062f_o.jpg" 35 | ] 36 | }, 37 | "presskit": "https://www.spacex.com/sites/spacex/files/crs-20_mission_press_kit.pdf", 38 | "webcast": "https://youtu.be/1MkcWK2PnsU", 39 | "youtube_id": "1MkcWK2PnsU", 40 | "article": "https://spaceflightnow.com/2020/03/07/late-night-launch-of-spacex-cargo-ship-marks-end-of-an-era/", 41 | "wikipedia": "https://en.wikipedia.org/wiki/SpaceX_CRS-20" 42 | }, 43 | "static_fire_date_utc": "2020-03-01T10:20:00.000Z", 44 | "static_fire_date_unix": 1583058000, 45 | "tdb": false, 46 | "net": false, 47 | "window": 0, 48 | "rocket": "5e9d0d95eda69973a809d1ec", 49 | "success": true, 50 | "failures": [], 51 | "details": "SpaceX's 20th and final Crew Resupply Mission under the original NASA CRS contract, this mission brings essential supplies to the International Space Station using SpaceX's reusable Dragon spacecraft. It is the last scheduled flight of a Dragon 1 capsule. (CRS-21 and up under the new Commercial Resupply Services 2 contract will use Dragon 2.) The external payload for this mission is the Bartolomeo ISS external payload hosting platform. Falcon 9 and Dragon will launch from SLC-40, Cape Canaveral Air Force Station and the booster will land at LZ-1. The mission will be complete with return and recovery of the Dragon capsule and down cargo.", 52 | "crew": [], 53 | "ships": [], 54 | "capsules": [ 55 | "5e9e2c5cf359185d753b266f" 56 | ], 57 | "payloads": [ 58 | "5eb0e4d0b6c3bb0006eeb253" 59 | ], 60 | "launchpad": "5e9e4501f509094ba4566f84", 61 | "auto_update": true, 62 | "flight_number": 91, 63 | "name": "CRS-20", 64 | "date_utc": "2020-03-07T04:50:31.000Z", 65 | "date_unix": 1583556631, 66 | "date_local": "2020-03-06T23:50:31-05:00", 67 | "date_precision": "hour", 68 | "upcoming": false, 69 | "cores": [ 70 | { 71 | "core": "5e9e28a7f359187afd3b2662", 72 | "flight": 2, 73 | "gridfins": true, 74 | "legs": true, 75 | "reused": true, 76 | "landing_attempt": true, 77 | "landing_success": true, 78 | "landing_type": "RTLS", 79 | "landpad": "5e9e3032383ecb267a34e7c7" 80 | } 81 | ], 82 | "id": "5eb87d42ffd86e000604b384" 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/launches/v5/latest.md: -------------------------------------------------------------------------------- 1 | # Get latest launch 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v5/launches/latest` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | { 15 | "fairings": null, 16 | "links": { 17 | "patch": { 18 | "small": "https://images2.imgbox.com/53/22/dh0XSLXO_o.png", 19 | "large": "https://images2.imgbox.com/15/2b/NAcsTEB6_o.png" 20 | }, 21 | "reddit": { 22 | "campaign": "https://www.reddit.com/r/spacex/comments/ezn6n0/crs20_launch_campaign_thread", 23 | "launch": "https://www.reddit.com/r/spacex/comments/fe8pcj/rspacex_crs20_official_launch_discussion_updates/", 24 | "media": "https://www.reddit.com/r/spacex/comments/fes64p/rspacex_crs20_media_thread_videos_images_gifs/", 25 | "recovery": null 26 | }, 27 | "flickr": { 28 | "small": [], 29 | "original": [ 30 | "https://live.staticflickr.com/65535/49635401403_96f9c322dc_o.jpg", 31 | "https://live.staticflickr.com/65535/49636202657_e81210a3ca_o.jpg", 32 | "https://live.staticflickr.com/65535/49636202572_8831c5a917_o.jpg", 33 | "https://live.staticflickr.com/65535/49635401423_e0bef3e82f_o.jpg", 34 | "https://live.staticflickr.com/65535/49635985086_660be7062f_o.jpg" 35 | ] 36 | }, 37 | "presskit": "https://www.spacex.com/sites/spacex/files/crs-20_mission_press_kit.pdf", 38 | "webcast": "https://youtu.be/1MkcWK2PnsU", 39 | "youtube_id": "1MkcWK2PnsU", 40 | "article": "https://spaceflightnow.com/2020/03/07/late-night-launch-of-spacex-cargo-ship-marks-end-of-an-era/", 41 | "wikipedia": "https://en.wikipedia.org/wiki/SpaceX_CRS-20" 42 | }, 43 | "static_fire_date_utc": "2020-03-01T10:20:00.000Z", 44 | "static_fire_date_unix": 1583058000, 45 | "tdb": false, 46 | "net": false, 47 | "window": 0, 48 | "rocket": "5e9d0d95eda69973a809d1ec", 49 | "success": true, 50 | "failures": [], 51 | "details": "SpaceX's 20th and final Crew Resupply Mission under the original NASA CRS contract, this mission brings essential supplies to the International Space Station using SpaceX's reusable Dragon spacecraft. It is the last scheduled flight of a Dragon 1 capsule. (CRS-21 and up under the new Commercial Resupply Services 2 contract will use Dragon 2.) The external payload for this mission is the Bartolomeo ISS external payload hosting platform. Falcon 9 and Dragon will launch from SLC-40, Cape Canaveral Air Force Station and the booster will land at LZ-1. The mission will be complete with return and recovery of the Dragon capsule and down cargo.", 52 | "crew": [], 53 | "ships": [], 54 | "capsules": [ 55 | "5e9e2c5cf359185d753b266f" 56 | ], 57 | "payloads": [ 58 | "5eb0e4d0b6c3bb0006eeb253" 59 | ], 60 | "launchpad": "5e9e4501f509094ba4566f84", 61 | "auto_update": true, 62 | "flight_number": 91, 63 | "name": "CRS-20", 64 | "date_utc": "2020-03-07T04:50:31.000Z", 65 | "date_unix": 1583556631, 66 | "date_local": "2020-03-06T23:50:31-05:00", 67 | "date_precision": "hour", 68 | "upcoming": false, 69 | "cores": [ 70 | { 71 | "core": "5e9e28a7f359187afd3b2662", 72 | "flight": 2, 73 | "gridfins": true, 74 | "legs": true, 75 | "reused": true, 76 | "landing_attempt": true, 77 | "landing_success": true, 78 | "landing_type": "RTLS", 79 | "landpad": "5e9e3032383ecb267a34e7c7" 80 | } 81 | ], 82 | "id": "5eb87d42ffd86e000604b384" 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /jobs/capsules.js: -------------------------------------------------------------------------------- 1 | import got from 'got'; 2 | import { load } from 'cheerio'; 3 | import { logger } from '../middleware/index.js'; 4 | 5 | const API = process.env.SPACEX_API; 6 | const KEY = process.env.SPACEX_KEY; 7 | const HEALTHCHECK = process.env.CAPSULES_HEALTHCHECK; 8 | const REDDIT_CAPSULES = 'https://old.reddit.com/r/spacex/wiki/capsules'; 9 | 10 | /** 11 | * Update capsule landings/reuse count 12 | * @return {Promise} 13 | */ 14 | export default async () => { 15 | try { 16 | const capsules = await got.post(`${API}/capsules/query`, { 17 | json: { 18 | options: { 19 | pagination: false, 20 | }, 21 | }, 22 | resolveBodyOnly: true, 23 | responseType: 'json', 24 | }); 25 | 26 | const result = await got(REDDIT_CAPSULES); 27 | const $ = load(result.body); 28 | 29 | const v1Capsules = $('div.md:nth-child(2) > table:nth-child(8) > tbody:nth-child(2)').text(); 30 | const v1CapsuleRow = v1Capsules.split('\n').filter((v) => v !== ''); 31 | const v1CapsuleIds = v1CapsuleRow.filter((value, index) => index % 7 === 0); 32 | if (!v1CapsuleIds.length) { 33 | throw new Error('No v1 capsules found'); 34 | } 35 | const v1CapsuleStatus = v1CapsuleRow.filter((value, index) => (index + 1) % 7 === 0).map((x) => x.replace(/\[source\]/gi, '')); 36 | 37 | const v2Capsules = $('div.md:nth-child(2) > table:nth-child(10) > tbody:nth-child(2)').text(); 38 | const v2CapsuleRow = v2Capsules.split('\n').filter((v) => v !== ''); 39 | const v2CapsuleIds = v2CapsuleRow.filter((value, index) => index % 8 === 0).map((x) => x.split(',')[0]); 40 | if (!v2CapsuleIds.length) { 41 | throw new Error('No v2 capsules found'); 42 | } 43 | const v2CapsuleStatus = v2CapsuleRow.filter((value, index) => (index + 1) % 8 === 0).map((x) => x.replace(/\[source\]/gi, '')); 44 | 45 | const capsuleIds = [...v1CapsuleIds, ...v2CapsuleIds]; 46 | const capsuleStatus = [...v1CapsuleStatus, ...v2CapsuleStatus]; 47 | 48 | const updates = capsules.docs.map(async (capsule) => { 49 | const waterLandings = await got.post(`${API}/payloads/query`, { 50 | json: { 51 | query: { 52 | 'dragon.capsule': capsule.id, 53 | 'dragon.water_landing': true, 54 | }, 55 | options: { 56 | pagination: false, 57 | }, 58 | }, 59 | resolveBodyOnly: true, 60 | responseType: 'json', 61 | }); 62 | 63 | const landLandings = await got.post(`${API}/payloads/query`, { 64 | json: { 65 | query: { 66 | 'dragon.capsule': capsule.id, 67 | 'dragon.land_landing': true, 68 | }, 69 | options: { 70 | pagination: false, 71 | }, 72 | }, 73 | resolveBodyOnly: true, 74 | responseType: 'json', 75 | }); 76 | 77 | const index = capsuleIds.findIndex((id) => id === capsule.serial); 78 | await got.patch(`${API}/capsules/${capsule.id}`, { 79 | json: { 80 | reuse_count: (capsule.launches.length > 0) ? capsule.launches.length - 1 : 0, 81 | water_landings: waterLandings.totalDocs, 82 | land_landings: landLandings.totalDocs, 83 | last_update: capsuleStatus[parseInt(index, 10)], 84 | }, 85 | headers: { 86 | 'spacex-key': KEY, 87 | }, 88 | }); 89 | }); 90 | 91 | await Promise.all(updates); 92 | 93 | logger.info('Capsules updated'); 94 | 95 | if (HEALTHCHECK) { 96 | await got(HEALTHCHECK); 97 | } 98 | } catch (error) { 99 | console.log(`Capsules Error: ${error.message}`); 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /docs/rockets/v4/all.md: -------------------------------------------------------------------------------- 1 | # Get all rockets 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/rockets` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "height": { 17 | "meters": 70, 18 | "feet": 229.6 19 | }, 20 | "diameter": { 21 | "meters": 12.2, 22 | "feet": 39.9 23 | }, 24 | "mass": { 25 | "kg": 1420788, 26 | "lb": 3125735 27 | }, 28 | "first_stage": { 29 | "thrust_sea_level": { 30 | "kN": 22819, 31 | "lbf": 5130000 32 | }, 33 | "thrust_vacuum": { 34 | "kN": 24681, 35 | "lbf": 5548500 36 | }, 37 | "reusable": true, 38 | "engines": 27, 39 | "fuel_amount_tons": 1155, 40 | "burn_time_sec": 162 41 | }, 42 | "second_stage": { 43 | "thrust": { 44 | "kN": 934, 45 | "lbf": 210000 46 | }, 47 | "payloads": { 48 | "composite_fairing": { 49 | "height": { 50 | "meters": 13.1, 51 | "feet": 43 52 | }, 53 | "diameter": { 54 | "meters": 5.2, 55 | "feet": 17.1 56 | } 57 | }, 58 | "option_1": "dragon" 59 | }, 60 | "reusable": false, 61 | "engines": 1, 62 | "fuel_amount_tons": 90, 63 | "burn_time_sec": 397 64 | }, 65 | "engines": { 66 | "isp": { 67 | "sea_level": 288, 68 | "vacuum": 312 69 | }, 70 | "thrust_sea_level": { 71 | "kN": 845, 72 | "lbf": 190000 73 | }, 74 | "thrust_vacuum": { 75 | "kN": 914, 76 | "lbf": 205500 77 | }, 78 | "number": 27, 79 | "type": "merlin", 80 | "version": "1D+", 81 | "layout": "octaweb", 82 | "engine_loss_max": 6, 83 | "propellant_1": "liquid oxygen", 84 | "propellant_2": "RP-1 kerosene", 85 | "thrust_to_weight": 180.1 86 | }, 87 | "landing_legs": { 88 | "number": 12, 89 | "material": "carbon fiber" 90 | }, 91 | "payload_weights": [ 92 | { 93 | "id": "leo", 94 | "name": "Low Earth Orbit", 95 | "kg": 63800, 96 | "lb": 140660 97 | }, 98 | { 99 | "id": "gto", 100 | "name": "Geosynchronous Transfer Orbit", 101 | "kg": 26700, 102 | "lb": 58860 103 | }, 104 | { 105 | "id": "mars", 106 | "name": "Mars Orbit", 107 | "kg": 16800, 108 | "lb": 37040 109 | }, 110 | { 111 | "id": "pluto", 112 | "name": "Pluto Orbit", 113 | "kg": 3500, 114 | "lb": 7720 115 | } 116 | ], 117 | "flickr_images": [ 118 | "https://farm5.staticflickr.com/4599/38583829295_581f34dd84_b.jpg", 119 | "https://farm5.staticflickr.com/4645/38583830575_3f0f7215e6_b.jpg", 120 | "https://farm5.staticflickr.com/4696/40126460511_b15bf84c85_b.jpg", 121 | "https://farm5.staticflickr.com/4711/40126461411_aabc643fd8_b.jpg" 122 | ], 123 | "name": "Falcon Heavy", 124 | "type": "rocket", 125 | "active": true, 126 | "stages": 2, 127 | "boosters": 2, 128 | "cost_per_launch": 90000000, 129 | "success_rate_pct": 100, 130 | "first_flight": "2018-02-06", 131 | "country": "United States", 132 | "company": "SpaceX", 133 | "wikipedia": "https://en.wikipedia.org/wiki/Falcon_Heavy", 134 | "description": "With the ability to lift into orbit over 54 metric tons (119,000 lb)--a mass equivalent to a 737 jetliner loaded with passengers, crew, luggage and fuel--Falcon Heavy can lift more than twice the payload of the next closest operational vehicle, the Delta IV Heavy, at one-third the cost.", 135 | "id": "5e9d0d95eda69974db09d1ed" 136 | }, 137 | ... 138 | ] 139 | ``` 140 | -------------------------------------------------------------------------------- /docs/launches/v4/one.md: -------------------------------------------------------------------------------- 1 | # Get one launch 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/launches/:id` 6 | 7 | **URL Parameters** : `id=[string]` where `id` is the ID of the launch 8 | 9 | **Auth required** : `False` 10 | 11 | ## Success Response 12 | 13 | **Code** : `200 OK` 14 | 15 | **Content example** : 16 | 17 | ```json 18 | { 19 | "fairings": null, 20 | "links": { 21 | "patch": { 22 | "small": "https://images2.imgbox.com/53/22/dh0XSLXO_o.png", 23 | "large": "https://images2.imgbox.com/15/2b/NAcsTEB6_o.png" 24 | }, 25 | "reddit": { 26 | "campaign": "https://www.reddit.com/r/spacex/comments/ezn6n0/crs20_launch_campaign_thread", 27 | "launch": "https://www.reddit.com/r/spacex/comments/fe8pcj/rspacex_crs20_official_launch_discussion_updates/", 28 | "media": "https://www.reddit.com/r/spacex/comments/fes64p/rspacex_crs20_media_thread_videos_images_gifs/", 29 | "recovery": null 30 | }, 31 | "flickr": { 32 | "small": [], 33 | "original": [ 34 | "https://live.staticflickr.com/65535/49635401403_96f9c322dc_o.jpg", 35 | "https://live.staticflickr.com/65535/49636202657_e81210a3ca_o.jpg", 36 | "https://live.staticflickr.com/65535/49636202572_8831c5a917_o.jpg", 37 | "https://live.staticflickr.com/65535/49635401423_e0bef3e82f_o.jpg", 38 | "https://live.staticflickr.com/65535/49635985086_660be7062f_o.jpg" 39 | ] 40 | }, 41 | "presskit": "https://www.spacex.com/sites/spacex/files/crs-20_mission_press_kit.pdf", 42 | "webcast": "https://youtu.be/1MkcWK2PnsU", 43 | "youtube_id": "1MkcWK2PnsU", 44 | "article": "https://spaceflightnow.com/2020/03/07/late-night-launch-of-spacex-cargo-ship-marks-end-of-an-era/", 45 | "wikipedia": "https://en.wikipedia.org/wiki/SpaceX_CRS-20" 46 | }, 47 | "static_fire_date_utc": "2020-03-01T10:20:00.000Z", 48 | "static_fire_date_unix": 1583058000, 49 | "tdb": false, 50 | "net": false, 51 | "window": 0, 52 | "rocket": "5e9d0d95eda69973a809d1ec", 53 | "success": true, 54 | "failures": [], 55 | "details": "SpaceX's 20th and final Crew Resupply Mission under the original NASA CRS contract, this mission brings essential supplies to the International Space Station using SpaceX's reusable Dragon spacecraft. It is the last scheduled flight of a Dragon 1 capsule. (CRS-21 and up under the new Commercial Resupply Services 2 contract will use Dragon 2.) The external payload for this mission is the Bartolomeo ISS external payload hosting platform. Falcon 9 and Dragon will launch from SLC-40, Cape Canaveral Air Force Station and the booster will land at LZ-1. The mission will be complete with return and recovery of the Dragon capsule and down cargo.", 56 | "crew": [], 57 | "ships": [], 58 | "capsules": [ 59 | "5e9e2c5cf359185d753b266f" 60 | ], 61 | "payloads": [ 62 | "5eb0e4d0b6c3bb0006eeb253" 63 | ], 64 | "launchpad": "5e9e4501f509094ba4566f84", 65 | "auto_update": true, 66 | "flight_number": 91, 67 | "name": "CRS-20", 68 | "date_utc": "2020-03-07T04:50:31.000Z", 69 | "date_unix": 1583556631, 70 | "date_local": "2020-03-06T23:50:31-05:00", 71 | "date_precision": "hour", 72 | "upcoming": false, 73 | "cores": [ 74 | { 75 | "core": "5e9e28a7f359187afd3b2662", 76 | "flight": 2, 77 | "gridfins": true, 78 | "legs": true, 79 | "reused": true, 80 | "landing_attempt": true, 81 | "landing_success": true, 82 | "landing_type": "RTLS", 83 | "landpad": "5e9e3032383ecb267a34e7c7" 84 | } 85 | ], 86 | "id": "5eb87d42ffd86e000604b384" 87 | } 88 | ``` 89 | 90 | ## Error Responses 91 | 92 | **Code** : `404 NOT FOUND` 93 | 94 | **Content** : `Not Found` 95 | -------------------------------------------------------------------------------- /docs/launches/v5/one.md: -------------------------------------------------------------------------------- 1 | # Get one launch 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v5/launches/:id` 6 | 7 | **URL Parameters** : `id=[string]` where `id` is the ID of the launch 8 | 9 | **Auth required** : `False` 10 | 11 | ## Success Response 12 | 13 | **Code** : `200 OK` 14 | 15 | **Content example** : 16 | 17 | ```json 18 | { 19 | "fairings": null, 20 | "links": { 21 | "patch": { 22 | "small": "https://images2.imgbox.com/53/22/dh0XSLXO_o.png", 23 | "large": "https://images2.imgbox.com/15/2b/NAcsTEB6_o.png" 24 | }, 25 | "reddit": { 26 | "campaign": "https://www.reddit.com/r/spacex/comments/ezn6n0/crs20_launch_campaign_thread", 27 | "launch": "https://www.reddit.com/r/spacex/comments/fe8pcj/rspacex_crs20_official_launch_discussion_updates/", 28 | "media": "https://www.reddit.com/r/spacex/comments/fes64p/rspacex_crs20_media_thread_videos_images_gifs/", 29 | "recovery": null 30 | }, 31 | "flickr": { 32 | "small": [], 33 | "original": [ 34 | "https://live.staticflickr.com/65535/49635401403_96f9c322dc_o.jpg", 35 | "https://live.staticflickr.com/65535/49636202657_e81210a3ca_o.jpg", 36 | "https://live.staticflickr.com/65535/49636202572_8831c5a917_o.jpg", 37 | "https://live.staticflickr.com/65535/49635401423_e0bef3e82f_o.jpg", 38 | "https://live.staticflickr.com/65535/49635985086_660be7062f_o.jpg" 39 | ] 40 | }, 41 | "presskit": "https://www.spacex.com/sites/spacex/files/crs-20_mission_press_kit.pdf", 42 | "webcast": "https://youtu.be/1MkcWK2PnsU", 43 | "youtube_id": "1MkcWK2PnsU", 44 | "article": "https://spaceflightnow.com/2020/03/07/late-night-launch-of-spacex-cargo-ship-marks-end-of-an-era/", 45 | "wikipedia": "https://en.wikipedia.org/wiki/SpaceX_CRS-20" 46 | }, 47 | "static_fire_date_utc": "2020-03-01T10:20:00.000Z", 48 | "static_fire_date_unix": 1583058000, 49 | "tdb": false, 50 | "net": false, 51 | "window": 0, 52 | "rocket": "5e9d0d95eda69973a809d1ec", 53 | "success": true, 54 | "failures": [], 55 | "details": "SpaceX's 20th and final Crew Resupply Mission under the original NASA CRS contract, this mission brings essential supplies to the International Space Station using SpaceX's reusable Dragon spacecraft. It is the last scheduled flight of a Dragon 1 capsule. (CRS-21 and up under the new Commercial Resupply Services 2 contract will use Dragon 2.) The external payload for this mission is the Bartolomeo ISS external payload hosting platform. Falcon 9 and Dragon will launch from SLC-40, Cape Canaveral Air Force Station and the booster will land at LZ-1. The mission will be complete with return and recovery of the Dragon capsule and down cargo.", 56 | "crew": [], 57 | "ships": [], 58 | "capsules": [ 59 | "5e9e2c5cf359185d753b266f" 60 | ], 61 | "payloads": [ 62 | "5eb0e4d0b6c3bb0006eeb253" 63 | ], 64 | "launchpad": "5e9e4501f509094ba4566f84", 65 | "auto_update": true, 66 | "flight_number": 91, 67 | "name": "CRS-20", 68 | "date_utc": "2020-03-07T04:50:31.000Z", 69 | "date_unix": 1583556631, 70 | "date_local": "2020-03-06T23:50:31-05:00", 71 | "date_precision": "hour", 72 | "upcoming": false, 73 | "cores": [ 74 | { 75 | "core": "5e9e28a7f359187afd3b2662", 76 | "flight": 2, 77 | "gridfins": true, 78 | "legs": true, 79 | "reused": true, 80 | "landing_attempt": true, 81 | "landing_success": true, 82 | "landing_type": "RTLS", 83 | "landpad": "5e9e3032383ecb267a34e7c7" 84 | } 85 | ], 86 | "id": "5eb87d42ffd86e000604b384" 87 | } 88 | ``` 89 | 90 | ## Error Responses 91 | 92 | **Code** : `404 NOT FOUND` 93 | 94 | **Content** : `Not Found` 95 | -------------------------------------------------------------------------------- /routes/launches/v5/index.js: -------------------------------------------------------------------------------- 1 | import Router from 'koa-router'; 2 | import { Launch } from '../../../models/index.js'; 3 | import { auth, authz, cache } from '../../../middleware/index.js'; 4 | 5 | const router = new Router({ 6 | prefix: '/(v5|latest)/launches', 7 | }); 8 | 9 | // 10 | // Convenience Endpoints 11 | // 12 | 13 | // Get past launches 14 | router.get('/past', cache(20), async (ctx) => { 15 | try { 16 | const result = await Launch.find({ 17 | upcoming: false, 18 | }, null, { 19 | sort: { 20 | flight_number: 'asc', 21 | }, 22 | }); 23 | ctx.status = 200; 24 | ctx.body = result; 25 | } catch (error) { 26 | ctx.throw(400, error.message); 27 | } 28 | }); 29 | 30 | // Get upcoming launches 31 | router.get('/upcoming', cache(20), async (ctx) => { 32 | try { 33 | const result = await Launch.find({ 34 | upcoming: true, 35 | }, null, { 36 | sort: { 37 | flight_number: 'asc', 38 | }, 39 | }); 40 | ctx.status = 200; 41 | ctx.body = result; 42 | } catch (error) { 43 | ctx.throw(400, error.message); 44 | } 45 | }); 46 | 47 | // Get latest launch 48 | router.get('/latest', cache(20), async (ctx) => { 49 | try { 50 | const result = await Launch.findOne({ 51 | upcoming: false, 52 | }, null, { 53 | sort: { 54 | flight_number: 'desc', 55 | }, 56 | }); 57 | ctx.status = 200; 58 | ctx.body = result; 59 | } catch (error) { 60 | ctx.throw(400, error.message); 61 | } 62 | }); 63 | 64 | // Get next launch 65 | router.get('/next', cache(20), async (ctx) => { 66 | try { 67 | const result = await Launch.findOne({ 68 | upcoming: true, 69 | }, null, { 70 | sort: { 71 | flight_number: 'asc', 72 | }, 73 | }); 74 | ctx.status = 200; 75 | ctx.body = result; 76 | } catch (error) { 77 | ctx.throw(400, error.message); 78 | } 79 | }); 80 | 81 | // 82 | // Standard Endpoints 83 | // 84 | 85 | // Get all launches 86 | router.get('/', cache(20), async (ctx) => { 87 | try { 88 | const result = await Launch.find({}, null, { 89 | sort: { 90 | flight_number: 'asc', 91 | }, 92 | }); 93 | ctx.status = 200; 94 | ctx.body = result; 95 | } catch (error) { 96 | ctx.throw(400, error.message); 97 | } 98 | }); 99 | 100 | // Get one launch 101 | router.get('/:id', cache(20), async (ctx) => { 102 | const result = await Launch.findById(ctx.params.id); 103 | if (!result) { 104 | ctx.throw(404); 105 | } 106 | ctx.status = 200; 107 | ctx.body = result; 108 | }); 109 | 110 | // Query launches 111 | router.post('/query', cache(20), async (ctx) => { 112 | const { query = {}, options = {} } = ctx.request.body; 113 | try { 114 | const result = await Launch.paginate(query, options); 115 | ctx.status = 200; 116 | ctx.body = result; 117 | } catch (error) { 118 | ctx.throw(400, error.message); 119 | } 120 | }); 121 | 122 | // Create a launch 123 | router.post('/', auth, authz('launch:create'), async (ctx) => { 124 | try { 125 | const launch = new Launch(ctx.request.body); 126 | await launch.save(); 127 | ctx.status = 201; 128 | } catch (error) { 129 | ctx.throw(400, error.message); 130 | } 131 | }); 132 | 133 | // Update a launch 134 | router.patch('/:id', auth, authz('launch:update'), async (ctx) => { 135 | try { 136 | await Launch.findByIdAndUpdate(ctx.params.id, ctx.request.body, { 137 | runValidators: true, 138 | }); 139 | ctx.status = 200; 140 | } catch (error) { 141 | ctx.throw(400, error.message); 142 | } 143 | }); 144 | 145 | // Delete a launch 146 | router.delete('/:id', auth, authz('launch:delete'), async (ctx) => { 147 | try { 148 | await Launch.findByIdAndDelete(ctx.params.id); 149 | ctx.status = 200; 150 | } catch (error) { 151 | ctx.throw(400, error.message); 152 | } 153 | }); 154 | 155 | export default router; 156 | -------------------------------------------------------------------------------- /docs/launches/v4/all.md: -------------------------------------------------------------------------------- 1 | # Get all launches 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/launches` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "fairings": null, 17 | "links": { 18 | "patch": { 19 | "small": "https://images2.imgbox.com/53/22/dh0XSLXO_o.png", 20 | "large": "https://images2.imgbox.com/15/2b/NAcsTEB6_o.png" 21 | }, 22 | "reddit": { 23 | "campaign": "https://www.reddit.com/r/spacex/comments/ezn6n0/crs20_launch_campaign_thread", 24 | "launch": "https://www.reddit.com/r/spacex/comments/fe8pcj/rspacex_crs20_official_launch_discussion_updates/", 25 | "media": "https://www.reddit.com/r/spacex/comments/fes64p/rspacex_crs20_media_thread_videos_images_gifs/", 26 | "recovery": null 27 | }, 28 | "flickr": { 29 | "small": [], 30 | "original": [ 31 | "https://live.staticflickr.com/65535/49635401403_96f9c322dc_o.jpg", 32 | "https://live.staticflickr.com/65535/49636202657_e81210a3ca_o.jpg", 33 | "https://live.staticflickr.com/65535/49636202572_8831c5a917_o.jpg", 34 | "https://live.staticflickr.com/65535/49635401423_e0bef3e82f_o.jpg", 35 | "https://live.staticflickr.com/65535/49635985086_660be7062f_o.jpg" 36 | ] 37 | }, 38 | "presskit": "https://www.spacex.com/sites/spacex/files/crs-20_mission_press_kit.pdf", 39 | "webcast": "https://youtu.be/1MkcWK2PnsU", 40 | "youtube_id": "1MkcWK2PnsU", 41 | "article": "https://spaceflightnow.com/2020/03/07/late-night-launch-of-spacex-cargo-ship-marks-end-of-an-era/", 42 | "wikipedia": "https://en.wikipedia.org/wiki/SpaceX_CRS-20" 43 | }, 44 | "static_fire_date_utc": "2020-03-01T10:20:00.000Z", 45 | "static_fire_date_unix": 1583058000, 46 | "tdb": false, 47 | "net": false, 48 | "window": 0, 49 | "rocket": "5e9d0d95eda69973a809d1ec", 50 | "success": true, 51 | "failures": [], 52 | "details": "SpaceX's 20th and final Crew Resupply Mission under the original NASA CRS contract, this mission brings essential supplies to the International Space Station using SpaceX's reusable Dragon spacecraft. It is the last scheduled flight of a Dragon 1 capsule. (CRS-21 and up under the new Commercial Resupply Services 2 contract will use Dragon 2.) The external payload for this mission is the Bartolomeo ISS external payload hosting platform. Falcon 9 and Dragon will launch from SLC-40, Cape Canaveral Air Force Station and the booster will land at LZ-1. The mission will be complete with return and recovery of the Dragon capsule and down cargo.", 53 | "crew": [], 54 | "ships": [], 55 | "capsules": [ 56 | "5e9e2c5cf359185d753b266f" 57 | ], 58 | "payloads": [ 59 | "5eb0e4d0b6c3bb0006eeb253" 60 | ], 61 | "launchpad": "5e9e4501f509094ba4566f84", 62 | "auto_update": true, 63 | "flight_number": 91, 64 | "name": "CRS-20", 65 | "date_utc": "2020-03-07T04:50:31.000Z", 66 | "date_unix": 1583556631, 67 | "date_local": "2020-03-06T23:50:31-05:00", 68 | "date_precision": "hour", 69 | "upcoming": false, 70 | "cores": [ 71 | { 72 | "core": "5e9e28a7f359187afd3b2662", 73 | "flight": 2, 74 | "gridfins": true, 75 | "legs": true, 76 | "reused": true, 77 | "landing_attempt": true, 78 | "landing_success": true, 79 | "landing_type": "RTLS", 80 | "landpad": "5e9e3032383ecb267a34e7c7" 81 | } 82 | ], 83 | "id": "5eb87d42ffd86e000604b384" 84 | }, 85 | ... 86 | ] 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/launches/v5/all.md: -------------------------------------------------------------------------------- 1 | # Get all launches 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v5/launches` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "fairings": null, 17 | "links": { 18 | "patch": { 19 | "small": "https://images2.imgbox.com/53/22/dh0XSLXO_o.png", 20 | "large": "https://images2.imgbox.com/15/2b/NAcsTEB6_o.png" 21 | }, 22 | "reddit": { 23 | "campaign": "https://www.reddit.com/r/spacex/comments/ezn6n0/crs20_launch_campaign_thread", 24 | "launch": "https://www.reddit.com/r/spacex/comments/fe8pcj/rspacex_crs20_official_launch_discussion_updates/", 25 | "media": "https://www.reddit.com/r/spacex/comments/fes64p/rspacex_crs20_media_thread_videos_images_gifs/", 26 | "recovery": null 27 | }, 28 | "flickr": { 29 | "small": [], 30 | "original": [ 31 | "https://live.staticflickr.com/65535/49635401403_96f9c322dc_o.jpg", 32 | "https://live.staticflickr.com/65535/49636202657_e81210a3ca_o.jpg", 33 | "https://live.staticflickr.com/65535/49636202572_8831c5a917_o.jpg", 34 | "https://live.staticflickr.com/65535/49635401423_e0bef3e82f_o.jpg", 35 | "https://live.staticflickr.com/65535/49635985086_660be7062f_o.jpg" 36 | ] 37 | }, 38 | "presskit": "https://www.spacex.com/sites/spacex/files/crs-20_mission_press_kit.pdf", 39 | "webcast": "https://youtu.be/1MkcWK2PnsU", 40 | "youtube_id": "1MkcWK2PnsU", 41 | "article": "https://spaceflightnow.com/2020/03/07/late-night-launch-of-spacex-cargo-ship-marks-end-of-an-era/", 42 | "wikipedia": "https://en.wikipedia.org/wiki/SpaceX_CRS-20" 43 | }, 44 | "static_fire_date_utc": "2020-03-01T10:20:00.000Z", 45 | "static_fire_date_unix": 1583058000, 46 | "tdb": false, 47 | "net": false, 48 | "window": 0, 49 | "rocket": "5e9d0d95eda69973a809d1ec", 50 | "success": true, 51 | "failures": [], 52 | "details": "SpaceX's 20th and final Crew Resupply Mission under the original NASA CRS contract, this mission brings essential supplies to the International Space Station using SpaceX's reusable Dragon spacecraft. It is the last scheduled flight of a Dragon 1 capsule. (CRS-21 and up under the new Commercial Resupply Services 2 contract will use Dragon 2.) The external payload for this mission is the Bartolomeo ISS external payload hosting platform. Falcon 9 and Dragon will launch from SLC-40, Cape Canaveral Air Force Station and the booster will land at LZ-1. The mission will be complete with return and recovery of the Dragon capsule and down cargo.", 53 | "crew": [], 54 | "ships": [], 55 | "capsules": [ 56 | "5e9e2c5cf359185d753b266f" 57 | ], 58 | "payloads": [ 59 | "5eb0e4d0b6c3bb0006eeb253" 60 | ], 61 | "launchpad": "5e9e4501f509094ba4566f84", 62 | "auto_update": true, 63 | "flight_number": 91, 64 | "name": "CRS-20", 65 | "date_utc": "2020-03-07T04:50:31.000Z", 66 | "date_unix": 1583556631, 67 | "date_local": "2020-03-06T23:50:31-05:00", 68 | "date_precision": "hour", 69 | "upcoming": false, 70 | "cores": [ 71 | { 72 | "core": "5e9e28a7f359187afd3b2662", 73 | "flight": 2, 74 | "gridfins": true, 75 | "legs": true, 76 | "reused": true, 77 | "landing_attempt": true, 78 | "landing_success": true, 79 | "landing_type": "RTLS", 80 | "landpad": "5e9e3032383ecb267a34e7c7" 81 | } 82 | ], 83 | "id": "5eb87d42ffd86e000604b384" 84 | }, 85 | ... 86 | ] 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/launches/v4/past.md: -------------------------------------------------------------------------------- 1 | # Get all past launches 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v4/launches/past` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "fairings": null, 17 | "links": { 18 | "patch": { 19 | "small": "https://images2.imgbox.com/53/22/dh0XSLXO_o.png", 20 | "large": "https://images2.imgbox.com/15/2b/NAcsTEB6_o.png" 21 | }, 22 | "reddit": { 23 | "campaign": "https://www.reddit.com/r/spacex/comments/ezn6n0/crs20_launch_campaign_thread", 24 | "launch": "https://www.reddit.com/r/spacex/comments/fe8pcj/rspacex_crs20_official_launch_discussion_updates/", 25 | "media": "https://www.reddit.com/r/spacex/comments/fes64p/rspacex_crs20_media_thread_videos_images_gifs/", 26 | "recovery": null 27 | }, 28 | "flickr": { 29 | "small": [], 30 | "original": [ 31 | "https://live.staticflickr.com/65535/49635401403_96f9c322dc_o.jpg", 32 | "https://live.staticflickr.com/65535/49636202657_e81210a3ca_o.jpg", 33 | "https://live.staticflickr.com/65535/49636202572_8831c5a917_o.jpg", 34 | "https://live.staticflickr.com/65535/49635401423_e0bef3e82f_o.jpg", 35 | "https://live.staticflickr.com/65535/49635985086_660be7062f_o.jpg" 36 | ] 37 | }, 38 | "presskit": "https://www.spacex.com/sites/spacex/files/crs-20_mission_press_kit.pdf", 39 | "webcast": "https://youtu.be/1MkcWK2PnsU", 40 | "youtube_id": "1MkcWK2PnsU", 41 | "article": "https://spaceflightnow.com/2020/03/07/late-night-launch-of-spacex-cargo-ship-marks-end-of-an-era/", 42 | "wikipedia": "https://en.wikipedia.org/wiki/SpaceX_CRS-20" 43 | }, 44 | "static_fire_date_utc": "2020-03-01T10:20:00.000Z", 45 | "static_fire_date_unix": 1583058000, 46 | "tdb": false, 47 | "net": false, 48 | "window": 0, 49 | "rocket": "5e9d0d95eda69973a809d1ec", 50 | "success": true, 51 | "failures": [], 52 | "details": "SpaceX's 20th and final Crew Resupply Mission under the original NASA CRS contract, this mission brings essential supplies to the International Space Station using SpaceX's reusable Dragon spacecraft. It is the last scheduled flight of a Dragon 1 capsule. (CRS-21 and up under the new Commercial Resupply Services 2 contract will use Dragon 2.) The external payload for this mission is the Bartolomeo ISS external payload hosting platform. Falcon 9 and Dragon will launch from SLC-40, Cape Canaveral Air Force Station and the booster will land at LZ-1. The mission will be complete with return and recovery of the Dragon capsule and down cargo.", 53 | "crew": [], 54 | "ships": [], 55 | "capsules": [ 56 | "5e9e2c5cf359185d753b266f" 57 | ], 58 | "payloads": [ 59 | "5eb0e4d0b6c3bb0006eeb253" 60 | ], 61 | "launchpad": "5e9e4501f509094ba4566f84", 62 | "auto_update": true, 63 | "flight_number": 91, 64 | "name": "CRS-20", 65 | "date_utc": "2020-03-07T04:50:31.000Z", 66 | "date_unix": 1583556631, 67 | "date_local": "2020-03-06T23:50:31-05:00", 68 | "date_precision": "hour", 69 | "upcoming": false, 70 | "cores": [ 71 | { 72 | "core": "5e9e28a7f359187afd3b2662", 73 | "flight": 2, 74 | "gridfins": true, 75 | "legs": true, 76 | "reused": true, 77 | "landing_attempt": true, 78 | "landing_success": true, 79 | "landing_type": "RTLS", 80 | "landpad": "5e9e3032383ecb267a34e7c7" 81 | } 82 | ], 83 | "id": "5eb87d42ffd86e000604b384" 84 | } 85 | ... 86 | ] 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/launches/v5/past.md: -------------------------------------------------------------------------------- 1 | # Get all past launches 2 | 3 | **Method** : `GET` 4 | 5 | **URL** : `https://api.spacexdata.com/v5/launches/past` 6 | 7 | **Auth required** : `False` 8 | 9 | ## Success Responses 10 | 11 | **Code** : `200 OK` 12 | 13 | ```json 14 | [ 15 | { 16 | "fairings": null, 17 | "links": { 18 | "patch": { 19 | "small": "https://images2.imgbox.com/53/22/dh0XSLXO_o.png", 20 | "large": "https://images2.imgbox.com/15/2b/NAcsTEB6_o.png" 21 | }, 22 | "reddit": { 23 | "campaign": "https://www.reddit.com/r/spacex/comments/ezn6n0/crs20_launch_campaign_thread", 24 | "launch": "https://www.reddit.com/r/spacex/comments/fe8pcj/rspacex_crs20_official_launch_discussion_updates/", 25 | "media": "https://www.reddit.com/r/spacex/comments/fes64p/rspacex_crs20_media_thread_videos_images_gifs/", 26 | "recovery": null 27 | }, 28 | "flickr": { 29 | "small": [], 30 | "original": [ 31 | "https://live.staticflickr.com/65535/49635401403_96f9c322dc_o.jpg", 32 | "https://live.staticflickr.com/65535/49636202657_e81210a3ca_o.jpg", 33 | "https://live.staticflickr.com/65535/49636202572_8831c5a917_o.jpg", 34 | "https://live.staticflickr.com/65535/49635401423_e0bef3e82f_o.jpg", 35 | "https://live.staticflickr.com/65535/49635985086_660be7062f_o.jpg" 36 | ] 37 | }, 38 | "presskit": "https://www.spacex.com/sites/spacex/files/crs-20_mission_press_kit.pdf", 39 | "webcast": "https://youtu.be/1MkcWK2PnsU", 40 | "youtube_id": "1MkcWK2PnsU", 41 | "article": "https://spaceflightnow.com/2020/03/07/late-night-launch-of-spacex-cargo-ship-marks-end-of-an-era/", 42 | "wikipedia": "https://en.wikipedia.org/wiki/SpaceX_CRS-20" 43 | }, 44 | "static_fire_date_utc": "2020-03-01T10:20:00.000Z", 45 | "static_fire_date_unix": 1583058000, 46 | "tdb": false, 47 | "net": false, 48 | "window": 0, 49 | "rocket": "5e9d0d95eda69973a809d1ec", 50 | "success": true, 51 | "failures": [], 52 | "details": "SpaceX's 20th and final Crew Resupply Mission under the original NASA CRS contract, this mission brings essential supplies to the International Space Station using SpaceX's reusable Dragon spacecraft. It is the last scheduled flight of a Dragon 1 capsule. (CRS-21 and up under the new Commercial Resupply Services 2 contract will use Dragon 2.) The external payload for this mission is the Bartolomeo ISS external payload hosting platform. Falcon 9 and Dragon will launch from SLC-40, Cape Canaveral Air Force Station and the booster will land at LZ-1. The mission will be complete with return and recovery of the Dragon capsule and down cargo.", 53 | "crew": [], 54 | "ships": [], 55 | "capsules": [ 56 | "5e9e2c5cf359185d753b266f" 57 | ], 58 | "payloads": [ 59 | "5eb0e4d0b6c3bb0006eeb253" 60 | ], 61 | "launchpad": "5e9e4501f509094ba4566f84", 62 | "auto_update": true, 63 | "flight_number": 91, 64 | "name": "CRS-20", 65 | "date_utc": "2020-03-07T04:50:31.000Z", 66 | "date_unix": 1583556631, 67 | "date_local": "2020-03-06T23:50:31-05:00", 68 | "date_precision": "hour", 69 | "upcoming": false, 70 | "cores": [ 71 | { 72 | "core": "5e9e28a7f359187afd3b2662", 73 | "flight": 2, 74 | "gridfins": true, 75 | "legs": true, 76 | "reused": true, 77 | "landing_attempt": true, 78 | "landing_success": true, 79 | "landing_type": "RTLS", 80 | "landpad": "5e9e3032383ecb267a34e7c7" 81 | } 82 | ], 83 | "id": "5eb87d42ffd86e000604b384" 84 | } 85 | ... 86 | ] 87 | ``` 88 | --------------------------------------------------------------------------------