diff --git a/.dockerignore b/.dockerignore index 3c3629e..654fe96 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,10 @@ +.git +.github +.nvmrc +dist +examples node_modules +README.md +ts.png +docker-compose.yml +eslint.config.js diff --git a/Dockerfile b/Dockerfile index f564d32..b64d379 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,15 @@ FROM node:24-slim ENV PNPM_HOME="/pnpm" +ENV COREPACK_HOME="/corepack" ENV PATH="$PNPM_HOME:$PATH" RUN corepack enable +RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/* RUN mkdir /app WORKDIR /app COPY package.json pnpm-lock.yaml ./ +RUN corepack install RUN pnpm install --frozen-lockfile --prod # copy in files @@ -17,5 +20,7 @@ COPY ./tsconfig.json \ EXPOSE 3000 ENV PORT=3000 +RUN chown -R node:node /app /pnpm /corepack +USER node -ENTRYPOINT [ "pnpm", "start" ] +ENTRYPOINT [ "tini", "--", "pnpm", "start" ] diff --git a/README.md b/README.md index 8696e88..4a18355 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ for usage... > It's recommended to specify the tag of the image you want rather than using the latest image, which might break. Image > tags are based off of the [release versions for json-server](https://github.com/typicode/json-server/releases). > However there is not an image for every version. See the available versions -> [here](https://hub.docker.com/r/codfish/json-server/tags). +> [on Docker Hub](https://hub.docker.com/r/codfish/json-server/tags). This project actually dogfoods itself. View the [docker-compose.yml](./docker-compose.yml) & the [examples/](./examples/) directory to see various usage examples. Also visit the @@ -87,6 +87,7 @@ This project actually dogfoods itself. View the [docker-compose.yml](./docker-co - [Typescript](./examples/typescript/) - [Mount in additional files](./examples/support-files/) - [Custom middleware](./examples/middlewares/) +- [Static files](./examples/static/) ### Docker Compose (Recommended) @@ -137,6 +138,9 @@ See all the [available options below](#options). ### Important Usage Notes +- **IDs must be strings.** json-server v1 uses strict equality (`===`) to match URL params against record IDs. Since URL + params are always strings, integer IDs will never match on individual resource lookups (e.g., `GET /users/1`). Use + string IDs like `"1"` or UUIDs. - All mounted files should use **ESM syntax** (`import`/`export default`). - All files should be mounted into the `/app` directory in the container. - The following files are special and will "just work" when **mounted over**: @@ -233,9 +237,14 @@ supported: | Option | Description | Default | | -------------- | ------------------------------------------------------------------------------------ | ------- | | `DEPENDENCIES` | Install extra npm dependencies in the container for you to use in your server files. | — | -| `STATIC` | Set static files directory | — | +| `STATIC` | Serve an additional static files directory (`./public` is always served) | — | | `PORT` | Set the port the server listens on inside the container. | 3000 | +> [!CAUTION] +> +> The `DEPENDENCIES` env var runs `pnpm add` with whatever packages you specify. A malicious package's install script +> will execute inside the container. Only use packages you trust. + ## Query Parameters json-server v1 supports the following query parameters: @@ -272,6 +281,7 @@ to a directory in [examples/](./examples/). | `docker-compose up json-db` | 9997 | Plain JSON database file | | `docker-compose up middlewares` | 9996 | Custom middleware that sets response headers | | `docker-compose up deps` | 9995 | Extra dependencies installed via `DEPENDENCIES` envar | +| `docker-compose up static` | 9993 | Custom public directory with static HTML | | `docker-compose up dags` | 9994 | Supporting files mounted alongside the db | Run all examples: diff --git a/db.js b/db.js index 07f7490..fd47eca 100644 --- a/db.js +++ b/db.js @@ -2,8 +2,8 @@ import { faker } from '@faker-js/faker'; export default () => ({ users: faker.helpers.multiple( - idx => ({ - id: idx, + () => ({ + id: faker.string.uuid(), name: faker.person.fullName(), username: faker.internet.username(), email: faker.internet.email(), diff --git a/docker-compose.yml b/docker-compose.yml index 9b26790..c561561 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,6 +62,19 @@ services: VIRTUAL_HOST: deps.json-server.docker DEPENDENCIES: chance@1 node-emoji@1 + static: + build: . + volumes: + - ./server.js:/app/server.js:delegated + - ./middleware.js:/app/middleware.js:delegated + - ./examples/static/db.json:/app/db.json + - ./examples/static/public:/app/public + ports: + - 9993:9993 + environment: + PORT: 9993 + VIRTUAL_HOST: static.json-server.docker + dags: build: . volumes: diff --git a/examples/deps/db.js b/examples/deps/db.js index 700c420..026883e 100644 --- a/examples/deps/db.js +++ b/examples/deps/db.js @@ -8,7 +8,7 @@ export default () => ({ const animal = chance.animal({ type: 'pet' }).replace(/s$/, '').split(' ').pop().toLowerCase(); return { - id: idx + 1, + id: `${idx + 1}`, animal, emoji: emoji.find(animal)?.emoji || '🦄', name: chance.name({ middle: true }), diff --git a/examples/json/db.json b/examples/json/db.json index 1c67cee..fdbeb0f 100644 --- a/examples/json/db.json +++ b/examples/json/db.json @@ -1,92 +1,92 @@ { "todos": [ { - "userId": 1, - "id": 1, + "userId": "1", + "id": "1", "title": "delectus aut autem", "completed": false }, { - "userId": 1, - "id": 2, + "userId": "1", + "id": "2", "title": "quis ut nam facilis et officia qui", "completed": false }, { - "userId": 1, - "id": 3, + "userId": "1", + "id": "3", "title": "fugiat veniam minus", "completed": false }, { - "userId": 1, - "id": 4, + "userId": "1", + "id": "4", "title": "et porro tempora", "completed": true }, { - "userId": 1, - "id": 5, + "userId": "1", + "id": "5", "title": "laboriosam mollitia et enim quasi adipisci quia provident illum", "completed": false }, { - "userId": 1, - "id": 6, + "userId": "1", + "id": "6", "title": "qui ullam ratione quibusdam voluptatem quia omnis", "completed": false }, { - "userId": 1, - "id": 7, + "userId": "1", + "id": "7", "title": "illo expedita consequatur quia in", "completed": false }, { - "userId": 1, - "id": 8, + "userId": "1", + "id": "8", "title": "quo adipisci enim quam ut ab", "completed": true }, { - "userId": 1, - "id": 9, + "userId": "1", + "id": "9", "title": "molestiae perspiciatis ipsa", "completed": false }, { - "userId": 1, - "id": 10, + "userId": "1", + "id": "10", "title": "illo est ratione doloremque quia maiores aut", "completed": true }, { - "userId": 1, - "id": 11, + "userId": "1", + "id": "11", "title": "vero rerum temporibus dolor", "completed": true }, { - "userId": 1, - "id": 12, + "userId": "1", + "id": "12", "title": "ipsa repellendus fugit nisi", "completed": true }, { - "userId": 1, - "id": 13, + "userId": "1", + "id": "13", "title": "et doloremque nulla", "completed": false }, { - "userId": 1, - "id": 14, + "userId": "1", + "id": "14", "title": "repellendus sunt dolores architecto voluptatum", "completed": true }, { - "userId": 1, - "id": 15, + "userId": "1", + "id": "15", "title": "ab voluptatum amet voluptas", "completed": true } diff --git a/examples/middlewares/db.json b/examples/middlewares/db.json index ef2b404..abfc34a 100644 --- a/examples/middlewares/db.json +++ b/examples/middlewares/db.json @@ -1,78 +1,78 @@ { "albums": [ { - "userId": 1, - "id": 1, + "userId": "1", + "id": "1", "title": "quidem molestiae enim" }, { - "userId": 1, - "id": 2, + "userId": "1", + "id": "2", "title": "sunt qui excepturi placeat culpa" }, { - "userId": 1, - "id": 3, + "userId": "1", + "id": "3", "title": "omnis laborum odio" }, { - "userId": 1, - "id": 4, + "userId": "1", + "id": "4", "title": "non esse culpa molestiae omnis sed optio" }, { - "userId": 1, - "id": 5, + "userId": "1", + "id": "5", "title": "eaque aut omnis a" }, { - "userId": 1, - "id": 6, + "userId": "1", + "id": "6", "title": "natus impedit quibusdam illo est" }, { - "userId": 1, - "id": 7, + "userId": "1", + "id": "7", "title": "quibusdam autem aliquid et et quia" }, { - "userId": 1, - "id": 8, + "userId": "1", + "id": "8", "title": "qui fuga est a eum" }, { - "userId": 1, - "id": 9, + "userId": "1", + "id": "9", "title": "saepe unde necessitatibus rem" }, { - "userId": 1, - "id": 10, + "userId": "1", + "id": "10", "title": "distinctio laborum qui" }, { - "userId": 2, - "id": 11, + "userId": "2", + "id": "11", "title": "quam nostrum impedit mollitia quod et dolor" }, { - "userId": 2, - "id": 12, + "userId": "2", + "id": "12", "title": "consequatur autem doloribus natus consectetur" }, { - "userId": 2, - "id": 13, + "userId": "2", + "id": "13", "title": "ab rerum non rerum consequatur ut ea unde" }, { - "userId": 2, - "id": 14, + "userId": "2", + "id": "14", "title": "ducimus molestias eos animi atque nihil" }, { - "userId": 2, - "id": 15, + "userId": "2", + "id": "15", "title": "ut pariatur rerum ipsum natus repellendus praesentium" } ] diff --git a/examples/static/db.json b/examples/static/db.json new file mode 100644 index 0000000..34c6b8f --- /dev/null +++ b/examples/static/db.json @@ -0,0 +1,14 @@ +{ + "posts": [ + { + "id": "1", + "title": "Hello World", + "body": "Welcome to json-server" + }, + { + "id": "2", + "title": "Second Post", + "body": "This is another post" + } + ] +} diff --git a/examples/static/public/image.png b/examples/static/public/image.png new file mode 100644 index 0000000..f1b136b Binary files /dev/null and b/examples/static/public/image.png differ diff --git a/examples/static/public/index.html b/examples/static/public/index.html new file mode 100644 index 0000000..4b327c2 --- /dev/null +++ b/examples/static/public/index.html @@ -0,0 +1,23 @@ + + + + + + JSON Server + + + +

JSON Server Custom index.html

+

Your mock API is running. Try these endpoints:

+ + + diff --git a/server.js b/server.js index f28b89d..3f25bc1 100755 --- a/server.js +++ b/server.js @@ -17,8 +17,8 @@ if (await fs.exists('.cache')) { } if (extraDeps && cache.trim() !== extraDeps) { - const command = `pnpm add --save-false ${process.env.DEPENDENCIES}`; - await $([command]); + const deps = process.env.DEPENDENCIES.split(/\s+/).filter(Boolean); + await $`pnpm add ${deps}`; await fs.writeFile('.cache', extraDeps); } @@ -67,7 +67,13 @@ if (await fs.pathExists('./dist/public')) { staticDirs.push('dist/public'); } if (process.env.STATIC) { - staticDirs.push(process.env.STATIC); + const staticPath = process.env.STATIC; + if (staticPath.includes('..') || staticPath.startsWith('/')) { + // eslint-disable-next-line no-console + console.error(`STATIC path must be relative and not contain '..'. Got: ${staticPath}`); + process.exit(1); + } + staticDirs.push(staticPath); } const app = createApp(db, { diff --git a/tsconfig.json b/tsconfig.json index 1e9923a..7927983 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,13 +1,10 @@ { "include": ["**/*.ts", "**/*.js"], - "exclude": ["dist", "examples", "node_modules"], + "exclude": ["dist/**", "examples/**", "node_modules/**"], "compilerOptions": { "target": "es2022", "module": "nodenext", "moduleResolution": "nodenext", - "paths": { - "@/*": ["./api/*"] - }, "allowJs": true, "checkJs": false, "outDir": "./dist",