diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index fe46b54345..f5dfa72aa0 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -17,6 +17,8 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + - name: Start MongoDB + uses: supercharge/mongodb-github-action@1.7.0 - run: npm install -g codeclimate-test-reporter - run: npm install - run: npm test diff --git a/package-lock.json b/package-lock.json index 8fc23de182..4dd828bf98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6506,9 +6506,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001355", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001355.tgz", - "integrity": "sha512-Sd6pjJHF27LzCB7pT7qs+kuX2ndurzCzkpJl6Qct7LPSZ9jn0bkOA8mdgMgmqnQAWLVOOGjLpc+66V57eLtb1g==", + "version": "1.0.30001356", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001356.tgz", + "integrity": "sha512-/30854bktMLhxtjieIxsrJBfs2gTM1pel6MXKF3K+RdIVJZcsn2A2QdhsuR4/p9+R204fZw0zCBBhktX8xWuyQ==", "funding": [ { "type": "opencollective", @@ -7961,9 +7961,9 @@ } }, "node_modules/eslint": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", - "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz", + "integrity": "sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.3.0", @@ -23096,9 +23096,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001355", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001355.tgz", - "integrity": "sha512-Sd6pjJHF27LzCB7pT7qs+kuX2ndurzCzkpJl6Qct7LPSZ9jn0bkOA8mdgMgmqnQAWLVOOGjLpc+66V57eLtb1g==" + "version": "1.0.30001356", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001356.tgz", + "integrity": "sha512-/30854bktMLhxtjieIxsrJBfs2gTM1pel6MXKF3K+RdIVJZcsn2A2QdhsuR4/p9+R204fZw0zCBBhktX8xWuyQ==" }, "center-align": { "version": "0.1.3", @@ -24221,9 +24221,9 @@ } }, "eslint": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", - "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.18.0.tgz", + "integrity": "sha512-As1EfFMVk7Xc6/CvhssHUjsAQSkpfXvUGMFC3ce8JDe6WvqCgRrLOBQbVpsBFr1X1V+RACOadnzVvcUS5ni2bA==", "dev": true, "requires": { "@eslint/eslintrc": "^1.3.0", diff --git a/packages/cli/src/app/index.ts b/packages/cli/src/app/index.ts index 6e0b3434f2..8cc917ec78 100644 --- a/packages/cli/src/app/index.ts +++ b/packages/cli/src/app/index.ts @@ -9,7 +9,8 @@ import { fromFile, install, copyFiles, - toFile + toFile, + when } from '@feathershq/pinion' import { FeathersBaseContext, FeathersAppInfo, initializeBaseContext } from '../commons' import { generate as authenticationGenerator } from '../authentication' @@ -110,7 +111,7 @@ export const generate = (ctx: AppGeneratorArguments) => message: 'What APIs do you want to offer?', choices: [ { value: 'rest', name: 'HTTP (REST)', checked: true }, - { value: 'websockets', name: 'Real-time (So)', checked: true } + { value: 'websockets', name: 'Real-time', checked: true } ] }, { @@ -180,30 +181,36 @@ export const generate = (ctx: AppGeneratorArguments) => .then(runGenerators(__dirname, 'templates')) .then(copyFiles(fromFile(__dirname, 'static'), toFile('.'))) .then(initializeBaseContext()) - .then(async (ctx) => { - if (ctx.database === 'custom') { - return ctx - } - - const { dependencies } = await connectionGenerator(ctx) + .then( + when( + ({ authStrategies, database }) => authStrategies.length > 0 && database !== 'custom', + async (ctx) => { + const { dependencies } = await connectionGenerator(ctx) - return { - ...ctx, - dependencies - } - }) - .then(async (ctx) => { - const { dependencies } = await authenticationGenerator({ - ...ctx, - service: 'users', - entity: 'user' - }) + return { + ...ctx, + dependencies + } + } + ) + ) + .then( + when( + ({ authStrategies }) => authStrategies.length > 0, + async (ctx) => { + const { dependencies } = await authenticationGenerator({ + ...ctx, + service: 'users', + entity: 'user' + }) - return { - ...ctx, - dependencies - } - }) + return { + ...ctx, + dependencies + } + } + ) + ) .then( install(({ transports, framework, dependencyVersions, dependencies }) => { const hasSocketio = transports.includes('websockets') diff --git a/packages/cli/src/app/templates/app.tpl.ts b/packages/cli/src/app/templates/app.tpl.ts index acbd88ca12..257651d807 100644 --- a/packages/cli/src/app/templates/app.tpl.ts +++ b/packages/cli/src/app/templates/app.tpl.ts @@ -31,7 +31,21 @@ app.configure(rest()) ${transports.includes('websockets') ? 'app.configure(socketio())' : ''} app.configure(services) app.configure(channels) -app.hooks([ logErrorHook ]) + +// Register hooks that run on all service methods +app.hooks({ + around: { + all: [ logErrorHook ] + }, + before: {}, + after: {}, + error: {} +}) +// Register application setup and teardown hooks here +app.hooks({ + setup: [], + teardown: [] +}) export { app } ` @@ -74,7 +88,21 @@ app.configure(channels) // Configure a middleware for 404s and the error handler app.use(notFound()) app.use(errorHandler({ logger })) -app.hooks([ logErrorHook ]) + +// Register hooks that run on all service methods +app.hooks({ + around: { + all: [ logErrorHook ] + }, + before: {}, + after: {}, + error: {} +}) +// Register application setup and teardown hooks here +app.hooks({ + setup: [], + teardown: [] +}) export { app } ` diff --git a/packages/cli/src/app/templates/readme.md.tpl.ts b/packages/cli/src/app/templates/readme.md.tpl.ts index da1e57ac22..16b6e94b0a 100644 --- a/packages/cli/src/app/templates/readme.md.tpl.ts +++ b/packages/cli/src/app/templates/readme.md.tpl.ts @@ -8,7 +8,7 @@ const template = ({ name, description }: AppGeneratorContext) => ## About -This project uses [Feathers](http://feathersjs.com). An open source web framework for building APIs and real-time applications. +This project uses [Feathers](http://feathersjs.com). An open source framework for building APIs and real-time applications. ## Getting Started @@ -38,7 +38,6 @@ Feathers has a powerful command line interface. Here are a few things it can do: $ npm install -g @feathersjs/cli # Install Feathers CLI $ feathers generate service # Generate a new Service -$ feathers generate hook # Generate a new Hook $ feathers help # Show all commands \`\`\` diff --git a/packages/cli/src/authentication/templates/authentication.tpl.ts b/packages/cli/src/authentication/templates/authentication.tpl.ts index cd50adae3e..4e3e74336f 100644 --- a/packages/cli/src/authentication/templates/authentication.tpl.ts +++ b/packages/cli/src/authentication/templates/authentication.tpl.ts @@ -2,10 +2,11 @@ import { generator, inject, before, toFile } from '@feathershq/pinion' import { getSource, renderSource } from '../../commons' import { AuthenticationGeneratorContext } from '../index' -const template = ({ authStrategies }: AuthenticationGeneratorContext) => +const template = ({ authStrategies, feathers }: AuthenticationGeneratorContext) => `import { AuthenticationService, JWTStrategy } from '@feathersjs/authentication' import { LocalStrategy } from '@feathersjs/authentication-local' -import { expressOauth } from '@feathersjs/authentication-oauth' +import { OAuthStrategy } from '@feathersjs/authentication-oauth' +${feathers.framework === 'express' ? `import { expressOauth } from '@feathersjs/authentication-oauth'` : ''} import type { Application } from './declarations' declare module './declarations' { @@ -18,10 +19,21 @@ export const authentication = (app: Application) => { const authentication = new AuthenticationService(app) authentication.register('jwt', new JWTStrategy()) - ${authStrategies.includes('local') ? "authentication.register('local', new LocalStrategy())" : ''} + ${authStrategies + .map( + (strategy) => + ` authentication.register('${strategy}', ${ + strategy === 'local' ? `new LocalStrategy()` : `new OAuthStrategy()` + })` + ) + .join('\n')} - app.use('authentication', authentication) - app.configure(expressOauth()) + app.use('authentication', authentication)${ + feathers.framework === 'express' + ? ` + app.configure(expressOauth())` + : '' + } } ` diff --git a/packages/cli/src/authentication/templates/test.tpl.ts b/packages/cli/src/authentication/templates/test.tpl.ts new file mode 100644 index 0000000000..d7bddc5486 --- /dev/null +++ b/packages/cli/src/authentication/templates/test.tpl.ts @@ -0,0 +1,50 @@ +import { generator, toFile } from '@feathershq/pinion' +import { renderSource } from '../../commons' +import { AuthenticationGeneratorContext } from '../index' + +const template = ({ authStrategies, relative, lib }: AuthenticationGeneratorContext) => + `import assert from 'assert'; +import { app } from '${relative}/${lib}/app'; + +describe('authentication', () => { + ${ + authStrategies.includes('local') + ? ` + const userInfo = { + email: 'someone@example.com', + password: 'supersecret' + } + + before(async () => { + try { + await app.service('users').create(userInfo) + } catch (error) { + // Do nothing, it just means the user already exists and can be tested + } + }); + + it('authenticates user and creates accessToken', async () => { + const { user, accessToken } = await app.service('authentication').create({ + strategy: 'local', + ...userInfo + }, {}) + + assert.ok(accessToken, 'Created access token for user') + assert.ok(user, 'Includes user in authentication data') + })` + : '' + } + + it('registered the authentication service', () => { + assert.ok(app.service('authentication')) + }) +}) +` + +export const generate = (ctx: AuthenticationGeneratorContext) => + generator(ctx).then( + renderSource( + template, + toFile(({ test }) => test, 'authentication.test') + ) + ) diff --git a/packages/cli/src/hook/index.ts b/packages/cli/src/hook/index.ts new file mode 100644 index 0000000000..5427b9c040 --- /dev/null +++ b/packages/cli/src/hook/index.ts @@ -0,0 +1,45 @@ +import { generator, prompt, runGenerators } from '@feathershq/pinion' +import _ from 'lodash' +import { FeathersBaseContext } from '../commons' + +export interface HookGeneratorContext extends FeathersBaseContext { + name: string + camelName: string + kebabName: string + type: 'regular' | 'around' +} + +export const generate = (ctx: HookGeneratorContext) => + generator(ctx) + .then( + prompt(({ type, name }) => [ + { + type: 'input', + name: 'name', + message: 'What is the name of the hook?', + when: !name + }, + { + name: 'type', + type: 'list', + when: !type, + message: 'What kind of hook is it?', + choices: [ + { value: 'around', name: 'Around' }, + { value: 'regular', name: 'Before, After or Error' } + ] + } + ]) + ) + .then((ctx) => { + const { name } = ctx + const kebabName = _.kebabCase(name) + const camelName = _.camelCase(name) + + return { + ...ctx, + kebabName, + camelName + } + }) + .then(runGenerators(__dirname, 'templates')) diff --git a/packages/cli/src/hook/templates/hook.tpl.ts b/packages/cli/src/hook/templates/hook.tpl.ts new file mode 100644 index 0000000000..35ce1bb99d --- /dev/null +++ b/packages/cli/src/hook/templates/hook.tpl.ts @@ -0,0 +1,28 @@ +import { generator, toFile } from '@feathershq/pinion' +import { HookGeneratorContext } from '../index' +import { renderSource } from '../../commons' + +const aroundTemplate = ({ camelName, name }: HookGeneratorContext) => ` +import { HookContext, NextFunction } from '../declarations' + +export const ${camelName} = async (context: HookContext, next: NextFunction) => { + console.log(\`Running hook ${name} on \${context.path}\.\${context.method}\`) + await next() +} +` + +const regularTemplate = ({ + camelName +}: HookGeneratorContext) => `import { HookContext } from '../declarations' + +export const ${camelName} = async (context: HookContext) => { + console.log(\`Running hook ${name} on \${context.path}\.\${context.method}\`) +}` + +export const generate = (ctx: HookGeneratorContext) => + generator(ctx).then( + renderSource( + (ctx) => (ctx.type === 'around' ? aroundTemplate(ctx) : regularTemplate(ctx)), + toFile(({ lib, kebabName }) => [lib, 'hooks', kebabName]) + ) + ) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index b00c3ea49f..256c125e44 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -20,6 +20,7 @@ export const command = (yargs: Argv) => yarg .command('app', 'Generate a new app', commandRunner) .command('service', 'Generate a service', commandRunner) + .command('hook', 'Generate a hook', commandRunner) ) .usage('Usage: $0 [options]') .help()