diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e20465c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +quote_type = single diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..e6e87c2 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,24 @@ +{ + "root": true, + "extends": [ + "@autotelic/eslint-config-js" + ], + "settings": { + "node": { + "version": "^18.x" + } + }, + "overrides": [{ + "files": ["*.ts"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"] + }], + "rules": { + "multiline-comment-style": "off" + } +} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..6aa047a --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,21 @@ +name: Validate & Release +on: + push: + tags: + - v* +jobs: + validate: + uses: ./.github/workflows/validate.yaml + + publish-npm: + runs-on: ubuntu-latest + needs: validate + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: .node-version + registry-url: https://registry.npmjs.org/ + - run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 8dd2576..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Validate & Release -on: - push: - tags: - - v[0-9]+.[0-9]+.[0-9]+ -jobs: - validate: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [14.x, 16.x] - - name: Node.js ${{ matrix.node }} - - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - run: npm run lint - - run: npm test - - publish-npm: - runs-on: ubuntu-latest - needs: validate - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v1 - with: - node-version: 16.x - registry-url: https://registry.npmjs.org/ - - run: npm publish --access public - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index ac4b5e8..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: test - -on: - push: - branches: [main] - pull_request: - branches: [main] - -jobs: - build: - runs-on: ubuntu-latest - - strategy: - matrix: - node-version: [14.x, 16.x] - - name: Node.js ${{ matrix.node }} - - steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm install - - run: npm run lint - - run: npm test diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml new file mode 100644 index 0000000..0440808 --- /dev/null +++ b/.github/workflows/validate.yaml @@ -0,0 +1,19 @@ +name: Lint & Test + +on: + workflow_call: + pull_request: + push: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: .node-version + registry-url: 'https://registry.npmjs.org' + - run: npm install + - run: npm run validate diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index 0b90aeb..0000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -$(npm bin)/lint-staged diff --git a/.node-version b/.node-version index d928989..a9d0873 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -16.15.1 +18.19.0 diff --git a/README.md b/README.md index 22c71a4..a123754 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Stream CSVs from Fastify routes. Uses [fast-csv](https://c2fo.github.io/fast-csv ## Installation -``` +```sh npm i -S @autotelic/fastify-stream-to-csv ``` @@ -43,19 +43,8 @@ fastify.get('/report', async function (req, reply) { ## Github Actions/Workflows -#### Getting Started - -* Create release and test workflows - ```sh - cd .github/workflows - cp release.yml.example release.yml - cp test.yml.example test.yml - ``` -* Update `release.yml` and `test.yml` with appropriate workflow for your plugin - -#### Triggering a Release - * Trigger the release workflow via release tag + ```sh git checkout main && git pull npm version { minor | major | path } diff --git a/examples/basic/index.js b/examples/basic/index.js index b4e046d..d1309b7 100644 --- a/examples/basic/index.js +++ b/examples/basic/index.js @@ -1,17 +1,16 @@ const { Readable } = require('stream') + const { fastifyStreamToCsv } = require('../../') -module.exports = async function (fastify, options) { +module.exports = async function csv (fastify, options) { fastify.register(fastifyStreamToCsv) - fastify.get('/report', async function (req, reply) { + fastify.get('/report', async function report (req, reply) { // create a readable stream const readStream = Readable.from(Array.from(Array(100000).keys())) // create a row formatter - const rowFormatter = num => { - return [`a${num}`, `b${num}`, `c${num}`] - } + const rowFormatter = num => [`a${num}`, `b${num}`, `c${num}`] // these are fast-csv format options const csvOptions = { diff --git a/examples/postgres-stream/index.js b/examples/postgres-stream/index.js index 200e941..e204185 100644 --- a/examples/postgres-stream/index.js +++ b/examples/postgres-stream/index.js @@ -1,13 +1,14 @@ const pg = require('pg') const QueryStream = require('pg-query-stream') + const { fastifyStreamToCsv } = require('../../') const connectionString = 'postgres://postgres:password@0.0.0.0:5432/postgres?sslmode=disable' -module.exports = async function (fastify, options) { +module.exports = async function csv (fastify, options) { fastify.register(fastifyStreamToCsv) - fastify.get('/db-report', async function (req, reply) { + fastify.get('/db-report', async function report (req, reply) { // create a readable stream const stream = new QueryStream('SELECT * FROM generate_series(0, $1) num', [10000]) const client = new pg.Client({ connectionString }) diff --git a/examples/typesense-iterator/index.js b/examples/typesense-iterator/index.js index 1fedb7c..dd66cc0 100644 --- a/examples/typesense-iterator/index.js +++ b/examples/typesense-iterator/index.js @@ -1,8 +1,10 @@ /* eslint-disable camelcase */ const { Readable } = require('stream') -const { fastifyStreamToCsv } = require('../../') + const Typesense = require('typesense') +const { fastifyStreamToCsv } = require('../../') + const client = new Typesense.Client({ nodes: [{ host: 'localhost', @@ -14,9 +16,11 @@ const client = new Typesense.Client({ }) // async generator we can turn into a Readable -const searchPager = async function * () { +async function * searchPager () { let total, shown - let page = 1; const per_page = 5 + let page = 1 + const per_page = 5 + do { const searchResults = await client.collections('books') .documents() @@ -34,6 +38,7 @@ const searchPager = async function * () { total = found shown = page * per_page page = page + 1 + // we want to yield each hit to be a csv row for (const hit of hits) { yield hit @@ -41,10 +46,10 @@ const searchPager = async function * () { } while (shown < total) } -module.exports = async function (fastify, options) { +module.exports = async function csv (fastify, options) { fastify.register(fastifyStreamToCsv) - fastify.get('/book-report', async function (req, reply) { + fastify.get('/book-report', async function report (req, reply) { // create a readable stream from our search pager const readStream = Readable.from(searchPager()) diff --git a/index.js b/index.js index fd229cb..6984ea4 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ 'use strict' -const fp = require('fastify-plugin') const { format } = require('@fast-csv/format') +const fp = require('fastify-plugin') async function replyDecorator ( readStream, diff --git a/package.json b/package.json index b022064..c8f345f 100644 --- a/package.json +++ b/package.json @@ -3,14 +3,19 @@ "version": "0.2.0", "description": "Stream CSVs from Fastify routes", "main": "index.js", - "directories": { - "test": "test" + "types": "types/index.d.ts", + "files": [ + "index.js", + "types/index.d.ts" + ], + "engines": { + "node": ">=18" }, "scripts": { + "lint": "eslint .", "test": "c8 --100 ava", - "lint": "standard", - "fix": "standard --fix", - "validate": "npm run lint && npm run test" + "test:types": "tsd", + "validate": "npm run lint && npm run test && npm run test:types" }, "repository": { "type": "git", @@ -24,26 +29,24 @@ }, "homepage": "https://github.com/autotelic/fastify-stream-to-csv#readme", "dependencies": { - "@fast-csv/format": "^4.3.5", - "fastify-plugin": "^3.0.0" + "@fast-csv/format": "^5.0.0", + "fastify-plugin": "^4.5.1" }, "devDependencies": { - "ava": "^4.0.1", - "c8": "^7.10.0", - "csv-parse": "^5.0.4", - "fastify": "^4.10.2", - "fastify-cli": "^5.7.0", - "husky": "^8.0.2", - "lint-staged": "^13.1.0", - "pg": "^8.7.3", - "pg-query-stream": "^4.2.3", - "standard": "^17.0.0", - "supertest": "^6.2.2", - "typesense": "^1.1.3" - }, - "lint-staged": { - "*.{js,jsx}": [ - "npm run fix" - ] + "@autotelic/eslint-config-js": "^0.2.1", + "@types/node": "^20.11.2", + "@typescript-eslint/eslint-plugin": "^6.19.0", + "@typescript-eslint/parser": "^6.19.0", + "ava": "^6.0.1", + "c8": "^9.1.0", + "csv-parse": "^5.5.3", + "eslint": "^8.56.0", + "fastify": "^4.25.2", + "fastify-cli": "^6.0.1", + "pg": "^8.11.3", + "pg-query-stream": "^4.5.3", + "supertest": "^6.3.4", + "tsd": "^0.30.3", + "typesense": "^1.7.2" } } diff --git a/test.js b/test.js index b01df92..6d9e460 100644 --- a/test.js +++ b/test.js @@ -1,8 +1,11 @@ +const { Readable } = require('stream') + +// eslint-disable-next-line import/no-unresolved const test = require('ava') +// eslint-disable-next-line import/no-unresolved +const { parse } = require('csv-parse/sync') const Fastify = require('fastify') const supertest = require('supertest') -const { Readable } = require('stream') -const { parse } = require('csv-parse/sync') const { fastifyStreamToCsv } = require('./index') @@ -22,12 +25,10 @@ test('Should create a CSV file:', async (t) => { fastify.register(fastifyStreamToCsv) - fastify.get('/', async function (req, reply) { + fastify.get('/', async function report (req, reply) { const readStream = Readable.from(Array.from(Array(1).keys())) - const rowFormatter = num => { - return [`a${num}`, `b${num}`, `c${num}`] - } + const rowFormatter = num => [`a${num}`, `b${num}`, `c${num}`] await reply.streamToCsv(readStream, rowFormatter, { csvOptions: { diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..c0b0da9 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,27 @@ +import { Readable } from 'stream' + +import { FormatterOptionsArgs } from '@fast-csv/format' +import type { FastifyPluginCallback } from 'fastify' + +export interface StreamToCsvOptions { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + csvOptions?: FormatterOptionsArgs +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type RowFormatterFunction = (row: any) => any[] + +declare module 'fastify' { + interface FastifyReply { + streamToCsv( + readStream: Readable, + rowFormatter: RowFormatterFunction, + options?: StreamToCsvOptions + ): void + } +} + +declare const fastifyStreamToCsv: FastifyPluginCallback + +export default fastifyStreamToCsv +export { fastifyStreamToCsv } diff --git a/types/index.test-d.ts b/types/index.test-d.ts new file mode 100644 index 0000000..ab57992 --- /dev/null +++ b/types/index.test-d.ts @@ -0,0 +1,40 @@ +import fastify, { FastifyInstance } from 'fastify' +// eslint-disable-next-line import/no-unresolved +import { expectAssignable, expectError } from 'tsd' + +import fastifyStreamToCsv from '..' + +const opt1 = {} + +const opt2 = { + csvOptions: { + objectMode: true, + delimiter: ',', + rowDelimiter: '\n', + quote: '"', + escape: '"', + quoteColumns: { column1: true }, + quoteHeaders: false, + headers: ['header1', 'header2'], + writeHeaders: true, + includeEndRowDelimiter: false, + writeBOM: false, + alwaysWriteHeaders: true + } +} + +expectAssignable(fastify().register(fastifyStreamToCsv, opt1)) +expectAssignable(fastify().register(fastifyStreamToCsv, opt2)) + +const errOpt1 = { + csvOptions: { + invalidOption: 'invalid' + } +} + +const errOpt2 = { + anotherInvalidKey: 'value' +} + +expectError(fastify().register(fastifyStreamToCsv, errOpt1)) +expectError(fastify().register(fastifyStreamToCsv, errOpt2))