diff --git a/Dockerfile b/Dockerfile index 8f28f85..3ac9b80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,8 +9,3 @@ COPY test.js . RUN npm install USER root - -# RUN chown node:node /usr/local/bin/cputil -# RUN chown node:node /usr/src/app -# EXPOSE 3000 -CMD ["/bin/sh", "-c", "npm test"] diff --git a/README.md b/README.md index ec86bf4..a544600 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,13 @@ The npm postinstall script has been removed as it fails on the GH action runner. ```js import fastifyCloudPrnt from '@autotelic/fastify-cloudprnt' +import view from '@fastify/view' export default async function basic (fastify, options) { - fastify.register(fastifyCloudPrnt, config) + // @fastify/view must be registered and available before fastify/view + await fastify.register(view, viewConfig) + + await fastify.register(fastifyCloudPrnt, config) } ``` @@ -41,7 +45,9 @@ See [examples](examples/) for working examples. ### Template Rendering -Templates should be in [Star Document Markup](https://star-m.jp/products/s_print/CloudPRNTSDK/Documentation/en/articles/markup/markupintro.html), and template rendering requires [`cputil`](https://star-m.jp/products/s_print/CloudPRNTSDK/Documentation/en/articles/cputil/cputil.html) to be in the path. The provided [Dockerfile](Dockerfile) builds a container with the app and `cputil` in. +Templates should be in [Star Document Markup](https://star-m.jp/products/s_print/CloudPRNTSDK/Documentation/en/articles/markup/markupintro.html), and template rendering requires [`cputil`](https://star-m.jp/products/s_print/CloudPRNTSDK/Documentation/en/articles/cputil/cputil.html) to be in the path. The provided [Dockerfile](Dockerfile) builds a container with the app and `cputil` in. Additionally, [@fastify/view](https://github.com/fastify/point-of-view) must be registered to fastify before fastify-cloudprnt. + +Star Micronics provides a [Star Document Markup Designer](https://star-document-markup-designer.smcs.site/) web app. ## API @@ -51,4 +57,18 @@ _@autotelic/fastify-cloudprnt_ exports a fastify plugin. When registered the plu * `getJob: () => token`: method that returns the url-safe string `token` for the next available print job on the queue. * `getJobData: (token) => object`: method that returns the data object for the job enqueued with the url-safe string `token`. * `deleteJob: (token) => any`: method that deletes the job enqueued with the url-safe string `token` from the print queue. -* `viewOptions: object`: object containing configuration options to be passed through to [point-of-view](https://github.com/fastify/point-of-view#options). The default option provided by this plugin is `engine: { nunjucks }`. +* `routePrefix: string`: string which will configure a prefix for all cloudprint routes. + +## Examples + +### Basic +An [example](./examples/basic/) fastify app using [node-cache](https://github.com/node-cache/node-cache). To run the basic example, use the following command: +```sh + npm run example:basic +``` + +### Redis +An [example](./examples/redis/) fastify app using [redis](https://www.npmjs.com/package/redis). To run the redis example, use the following command: +```sh + npm run example:redis +``` \ No newline at end of file diff --git a/app.js b/app.js deleted file mode 100644 index 0b9b150..0000000 --- a/app.js +++ /dev/null @@ -1,7 +0,0 @@ -async function pluginApp (fastify, options) { - fastify.get('/', async function (req, reply) { - return { hello: 'world' } - }) -} - -export default pluginApp diff --git a/docker-compose.basic-example.yaml b/docker-compose.basic-example.yaml new file mode 100644 index 0000000..3c7df84 --- /dev/null +++ b/docker-compose.basic-example.yaml @@ -0,0 +1,11 @@ +version: '3.5' + +services: + fastify-cloudprnt: + container_name: fastify-cloudprnt + build: + context: . + dockerfile: Dockerfile + ports: + - '3000:3000' + command: "npm run example:basic:start" diff --git a/docker-compose.redis-example.yaml b/docker-compose.redis-example.yaml new file mode 100644 index 0000000..0b0577c --- /dev/null +++ b/docker-compose.redis-example.yaml @@ -0,0 +1,20 @@ +version: '3.5' + +services: + redis: + container_name: cloudprnt-redis + image: redis + ports: + - '6379:6379' + fastify-cloudprnt: + container_name: fastify-cloudprnt + build: + context: . + dockerfile: Dockerfile + ports: + - '3000:3000' + environment: + - REDIS_URL=redis://host.docker.internal:6379 + depends_on: + - redis + command: "npm run example:redis:start" diff --git a/docker-compose.yml b/docker-compose.test.yaml similarity index 85% rename from docker-compose.yml rename to docker-compose.test.yaml index 2f45c97..0924992 100644 --- a/docker-compose.yml +++ b/docker-compose.test.yaml @@ -8,3 +8,4 @@ services: dockerfile: Dockerfile ports: - '3000:3000' + command: "npm run test" diff --git a/examples/basic/index.js b/examples/basic/index.js index 30766cf..90d85e4 100644 --- a/examples/basic/index.js +++ b/examples/basic/index.js @@ -1,7 +1,15 @@ import NodeCache from 'node-cache' +import view from '@fastify/view' +import nunjucks from 'nunjucks' + import fastifyCloudPrnt from '../../index.js' export default async function basic (fastify, options) { + await fastify.register(view, { + engine: { nunjucks }, + root: './examples' + }) + const printQueue = new NodeCache() fastify.register(fastifyCloudPrnt, { @@ -17,7 +25,6 @@ export default async function basic (fastify, options) { deleteJob: token => { const deleted = printQueue.del(token) return deleted > 0 - }, - viewOptions: { root: './examples' } + } }) } diff --git a/examples/redis/index.js b/examples/redis/index.js new file mode 100644 index 0000000..0f9dfce --- /dev/null +++ b/examples/redis/index.js @@ -0,0 +1,44 @@ +import { createClient } from 'redis' +import view from '@fastify/view' +import nunjucks from 'nunjucks' + +import fastifyCloudPrnt from '../../index.js' + +const JOB_DATA = 'printJobData' +const PRINT_QUEUE = 'printJobQueue' +const REDIS_URL = process.env.REDIS_URL + +export default async function basic (fastify, options) { + // redis connection url defaults to localhost on port 6379 + const redis = createClient({ url: REDIS_URL }) + await redis.connect() + + await fastify.register(view, { + engine: { nunjucks }, + root: './examples' + }) + + await fastify.register(fastifyCloudPrnt, { + queueJob: async (token, jobData) => { + await redis.HSET(JOB_DATA, token, JSON.stringify(jobData)) + await redis.LPUSH(PRINT_QUEUE, token) + }, + getJob: async () => { + const token = await redis.LINDEX(PRINT_QUEUE, 0) + return token === undefined ? null : token + }, + getJobData: async token => { + const jsonJobData = await redis.HGET(JOB_DATA, token) + if (!jsonJobData) { + return null + } + return JSON.parse(jsonJobData) + }, + deleteJob: async token => { + await redis.HDEL(JOB_DATA, token) + const deletedPrintJob = await redis.LREM(PRINT_QUEUE, 1, token) + + return deletedPrintJob > 0 + } + }) +} diff --git a/index.js b/index.js index 46f43fd..a2dc256 100644 --- a/index.js +++ b/index.js @@ -1,21 +1,17 @@ 'use strict' -import view from '@fastify/view' -import nunjucks from 'nunjucks' +import fp from 'fastify-plugin' import pollRoute from './routes/main/post.js' import getJobRoute from './routes/main/get.js' import queueJobRoute from './routes/job/post.js' import deleteJobRoute from './routes/main/delete.js' -const defaultViewOptions = { engine: { nunjucks } } - export const defaultOptions = { getJob: () => null, getJobData: () => ({}), queueJob: () => false, - deleteJob: () => false, - viewOptions: defaultViewOptions + deleteJob: () => false } async function fastifyCloudPrnt (fastify, options = defaultOptions) { @@ -24,26 +20,29 @@ async function fastifyCloudPrnt (fastify, options = defaultOptions) { getJobData, queueJob, deleteJob, - viewOptions + routePrefix } = { ...defaultOptions, ...options } - fastify.register(view, { - ...defaultViewOptions, - ...viewOptions + fastify.decorate('cloudPrnt', { + getJob, + getJobData, + queueJob, + deleteJob }) - fastify.decorate('getJob', getJob) - fastify.decorate('getJobData', getJobData) - fastify.decorate('queueJob', queueJob) - fastify.decorate('deleteJob', deleteJob) - - fastify.route(pollRoute) - fastify.route(getJobRoute) - fastify.route(queueJobRoute) - fastify.route(deleteJobRoute) + fastify.register(async function (f) { + f.route(pollRoute) + f.route(getJobRoute) + f.route(queueJobRoute) + f.route(deleteJobRoute) + }, { prefix: routePrefix }) } -export default fastifyCloudPrnt +export default fp(fastifyCloudPrnt, { + name: 'fastify-plugin', + decorators: ['view'], + dependencies: ['@fastify/view'] +}) diff --git a/package.json b/package.json index 10ffb68..e759802 100644 --- a/package.json +++ b/package.json @@ -4,48 +4,62 @@ "description": "Fastify plugin to run a server following the Star Micronics CloudPRNT protocol.", "main": "index.js", "type": "module", - "directories": { - "test": "test" - }, "scripts": { - "example:basic": "fastify start -w -l info -P -o examples/basic/index.js", + "example:basic": "docker compose -f docker-compose.basic-example.yaml up --build", + "example:basic:start": "fastify start -w -l info -P -o examples/basic/index.js", + "example:redis": "docker compose -f docker-compose.redis-example.yaml up --build", + "example:redis:start": "fastify start -w -l info -P -o examples/redis/index.js", "test": "c8 --100 ava", + "test:docker": "docker compose -f docker-compose.test.yaml up", "lint": "standard", "fix": "standard --fix", "validate": "npm run lint && npm run test" }, + "files": [ + "index.js", + "routes" + ], "repository": { "type": "git", - "url": "git+https://github.com/autotelic/fastify-plugin-template.git" + "url": "git+https://github.com/autotelic/fastify-cloudprnt.git" }, - "keywords": [], + "keywords": [ + "fastify", + "cloudprnt", + "cloud-prnt", + "star-micronics" + ], "author": "Autotelic Development Ltd ", "license": "MIT", "bugs": { - "url": "https://github.com/autotelic/fastify-plugin-template/issues" + "url": "https://github.com/autotelic/fastify-cloudprnt/issues" }, - "homepage": "https://github.com/autotelic/fastify-plugin-template#readme", + "homepage": "https://github.com/autotelic/fastify-cloudprnt#readme", "dependencies": { - "@fastify/view": "^7.0.0", - "@rauschma/stringio": "^1.4.0", "execa": "^6.1.0", - "fastify-cli": "^3.1.0", - "fastify-plugin": "^4.0.0", - "nunjucks": "^3.2.3" + "fastify-plugin": "^4.0.0" }, "devDependencies": { + "@fastify/view": "^7.0.0", "ava": "^4.3.0", "c8": "^7.10.0", "fastify": "^4.3.0", + "fastify-cli": "^3.1.0", "husky": "^8.0.1", "lint-staged": "^13.0.3", "node-cache": "^5.1.2", + "nunjucks": "^3.2.3", + "redis": "^4.5.1", "sinon": "^14.0.0", "standard": "^17.0.0" }, + "peerDependencies": { + "@fastify/view": "^7.0.0" + }, "lint-staged": { "*.{js,jsx}": [ "npm run fix" ] } } + diff --git a/routes/job/post.js b/routes/job/post.js index 2167672..78e1721 100644 --- a/routes/job/post.js +++ b/routes/job/post.js @@ -22,7 +22,7 @@ export default { }, handler: async function queueJobHandler (request, reply) { const { token, jobData } = request.body - this.queueJob(token, jobData) + this.cloudPrnt.queueJob(token, jobData) return reply .code(201) .send({ token }) diff --git a/routes/main/delete.js b/routes/main/delete.js index 6849af8..5314b5b 100644 --- a/routes/main/delete.js +++ b/routes/main/delete.js @@ -22,7 +22,7 @@ export default { }, handler: async function queueJobHandler (request, reply) { const { token } = request.query - const deleted = this.deleteJob(token) + const deleted = this.cloudPrnt.deleteJob(token) const code = deleted ? 200 : 404 diff --git a/routes/main/get.js b/routes/main/get.js index 8418f97..a50b267 100644 --- a/routes/main/get.js +++ b/routes/main/get.js @@ -20,7 +20,7 @@ export default { }, handler: async function getJobHandler (request, reply) { const { token } = request.query - const jobData = await this.getJobData(token) + const jobData = await this.cloudPrnt.getJobData(token) if (jobData === null) { return reply.code(404).send('Job not found') } diff --git a/routes/main/post.js b/routes/main/post.js index 37d610b..93df516 100644 --- a/routes/main/post.js +++ b/routes/main/post.js @@ -23,7 +23,7 @@ export default { } }, handler: async function pollHandler (request, reply) { - const jobToken = await this.getJob() + const jobToken = await this.cloudPrnt.getJob() const jobReady = jobToken !== null let jobReadyResponse = {} diff --git a/test.js b/test.js index 92e8dbc..146fbb7 100644 --- a/test.js +++ b/test.js @@ -1,8 +1,13 @@ import test from 'ava' import sinon from 'sinon' import Fastify from 'fastify' +import view from '@fastify/view' +import nunjucks from 'nunjucks' + import fastifyCloudPrnt, { defaultOptions } from './index.js' +const defaultViewOpts = { engine: { nunjucks } } + test('default options', async (t) => { t.deepEqual(defaultOptions.getJobData(), {}) t.is(defaultOptions.getJob(), null) @@ -13,6 +18,7 @@ test('default options', async (t) => { test('client poll, no job ready', async (t) => { const fastify = Fastify() + fastify.register(view, defaultViewOpts) fastify.register(fastifyCloudPrnt) await fastify.ready() @@ -31,6 +37,7 @@ test('client poll, job ready', async (t) => { const jobToken = 'ABC123' const fastify = Fastify() + fastify.register(view, defaultViewOpts) fastify.register(fastifyCloudPrnt, { getJob: () => jobToken }) @@ -57,6 +64,10 @@ test('get job, success', async (t) => { const jobToken = 'ABC123' const fastify = Fastify() + fastify.register(view, { + ...defaultViewOpts, + root: './examples' + }) fastify.register(fastifyCloudPrnt, { getJobData: (token) => ({}), viewOptions: { root: './examples' } @@ -77,6 +88,7 @@ test('get job, not found', async (t) => { const jobToken = 'ABC123' const fastify = Fastify() + fastify.register(view, defaultViewOpts) fastify.register(fastifyCloudPrnt, { getJobData: (token) => null }) @@ -97,6 +109,7 @@ test('delete job, success', async (t) => { const deleteJob = sinon.stub().returns(true) + fastify.register(view, defaultViewOpts) fastify.register(fastifyCloudPrnt, { deleteJob }) await fastify.ready() @@ -116,6 +129,7 @@ test('delete job, not found', async (t) => { const deleteJob = sinon.stub().returns(false) + fastify.register(view, defaultViewOpts) fastify.register(fastifyCloudPrnt, { deleteJob }) await fastify.ready() @@ -136,6 +150,7 @@ test('queue job', async (t) => { const queueJob = sinon.spy() + fastify.register(view, defaultViewOpts) fastify.register(fastifyCloudPrnt, { queueJob }) await fastify.ready() @@ -152,3 +167,50 @@ test('queue job', async (t) => { t.is(response.statusCode, 201) t.deepEqual(response.json(), { token }) }) + +test('should throw an error if @fastify/view is not registered', async t => { + const fastify = Fastify() + const register = async () => { + await fastify.register(fastifyCloudPrnt) + } + await t.throwsAsync(register) +}) + +test('should prefix routes when supplied with a routePrefix opt', async t => { + const token = 'ABC123' + const jobData = { foo: 'bar' } + const prefix = '/test' + const fastify = Fastify() + + const queueJob = sinon.spy() + + fastify.register(view, defaultViewOpts) + fastify.register(fastifyCloudPrnt, { + queueJob, + routePrefix: prefix + }) + await fastify.ready() + + const withPrefixResponse = await fastify.inject({ + method: 'POST', + url: `${prefix}/job`, + body: { + token, + jobData + } + }) + + t.true(queueJob.calledWith(token, jobData)) + t.is(withPrefixResponse.statusCode, 201) + + const noPrefixResponse = await fastify.inject({ + method: 'POST', + url: '/job', + body: { + token, + jobData + } + }) + + t.is(noPrefixResponse.statusCode, 404) +})