├── .npmignore ├── generators ├── app │ ├── configs │ │ ├── config.test.json.js │ │ ├── config.production.json.js │ │ ├── config.default.json.js │ │ ├── index.js │ │ ├── tsconfig.json.js │ │ ├── eslintrc.json.js │ │ └── package.json.js │ ├── templates │ │ ├── js │ │ │ ├── src │ │ │ │ ├── services │ │ │ │ │ └── index.js │ │ │ │ ├── middleware │ │ │ │ │ └── index.js │ │ │ │ ├── index.js │ │ │ │ ├── logger.js │ │ │ │ ├── app.hooks.js │ │ │ │ └── channels.js │ │ │ ├── README.md │ │ │ ├── app.test.jest.js │ │ │ ├── app.test.mocha.js │ │ │ ├── app.js │ │ │ └── _gitignore │ │ ├── static │ │ │ ├── public │ │ │ │ ├── favicon.ico │ │ │ │ └── index.html │ │ │ ├── .editorconfig │ │ │ └── .gitignore │ │ └── ts │ │ │ ├── jest.config.js │ │ │ ├── src │ │ │ ├── services │ │ │ │ └── index.ts │ │ │ ├── middleware │ │ │ │ └── index.ts │ │ │ ├── declarations.d.ts │ │ │ ├── index.ts │ │ │ ├── logger.ts │ │ │ ├── app.hooks.ts │ │ │ └── channels.ts │ │ │ ├── README.md │ │ │ ├── app.test.mocha.ts │ │ │ ├── app.test.jest.ts │ │ │ ├── app.ts │ │ │ └── _gitignore │ └── index.js ├── middleware │ ├── templates │ │ ├── js │ │ │ └── middleware.js │ │ └── ts │ │ │ └── middleware.ts │ └── index.js ├── service │ ├── templates │ │ ├── js │ │ │ ├── types │ │ │ │ ├── nedb.js │ │ │ │ ├── memory.js │ │ │ │ ├── mongoose.js │ │ │ │ ├── sequelize.js │ │ │ │ ├── knex.js │ │ │ │ ├── prisma.js │ │ │ │ ├── cassandra.js │ │ │ │ ├── objection.js │ │ │ │ ├── mongodb.js │ │ │ │ ├── couchbase.js │ │ │ │ └── generic.js │ │ │ ├── test.jest.js │ │ │ ├── model │ │ │ │ ├── nedb.js │ │ │ │ ├── nedb-user.js │ │ │ │ ├── knex.js │ │ │ │ ├── mongoose.js │ │ │ │ ├── sequelize.js │ │ │ │ ├── cassandra.js │ │ │ │ ├── knex-user.js │ │ │ │ ├── mongoose-user.js │ │ │ │ ├── sequelize-user.js │ │ │ │ ├── cassandra-user.js │ │ │ │ ├── objection.js │ │ │ │ └── objection-user.js │ │ │ ├── test.mocha.js │ │ │ ├── hooks.js │ │ │ ├── service.js │ │ │ └── hooks-user.js │ │ └── ts │ │ │ ├── test.jest.ts │ │ │ ├── types │ │ │ ├── cassandra.ts │ │ │ ├── couchbase.ts │ │ │ ├── nedb.ts │ │ │ ├── memory.ts │ │ │ ├── mongoose.ts │ │ │ ├── sequelize.ts │ │ │ ├── knex.ts │ │ │ ├── prisma.ts │ │ │ ├── objection.ts │ │ │ ├── mongodb.ts │ │ │ └── generic.ts │ │ │ ├── test.mocha.ts │ │ │ ├── model │ │ │ ├── nedb.ts │ │ │ ├── nedb-user.ts │ │ │ ├── knex.ts │ │ │ ├── mongoose.ts │ │ │ ├── cassandra.ts │ │ │ ├── sequelize.ts │ │ │ ├── knex-user.ts │ │ │ ├── mongoose-user.ts │ │ │ ├── sequelize-user.ts │ │ │ ├── cassandra-user.ts │ │ │ ├── objection.ts │ │ │ └── objection-user.ts │ │ │ ├── hooks.ts │ │ │ ├── service.ts │ │ │ └── hooks-user.ts │ └── index.js ├── connection │ ├── templates │ │ ├── js │ │ │ ├── knex.js │ │ │ ├── couchbase.js │ │ │ ├── mongoose.js │ │ │ ├── objection.js │ │ │ ├── prisma.js │ │ │ ├── mongodb.js │ │ │ ├── cassandra.js │ │ │ └── sequelize.js │ │ └── ts │ │ │ ├── couchbase.ts │ │ │ ├── knex.ts │ │ │ ├── mongoose.ts │ │ │ ├── objection.ts │ │ │ ├── mongodb.ts │ │ │ ├── prisma.ts │ │ │ ├── cassandra.ts │ │ │ └── sequelize.ts │ └── index.js ├── hook │ ├── templates │ │ ├── js │ │ │ └── hook.js │ │ └── ts │ │ │ └── hook.ts │ └── index.js ├── upgrade │ ├── templates │ │ ├── js │ │ │ └── authentication.js │ │ └── ts │ │ │ └── authentication.ts │ └── index.js └── authentication │ ├── templates │ ├── js │ │ ├── authentication.js │ │ ├── test.jest.js │ │ └── test.mocha.js │ └── ts │ │ ├── authentication.ts │ │ ├── test.jest.ts │ │ └── test.mocha.ts │ └── index.js ├── .editorconfig ├── meta.js ├── .eslintrc.json ├── .gitignore ├── .github └── workflows │ ├── nodejs.yml │ └── update-dependencies.yml ├── test ├── utils.js └── generators.test.js ├── LICENSE ├── README.md ├── package.json └── lib └── generator.js /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | src/ 3 | test/ 4 | -------------------------------------------------------------------------------- /generators/app/configs/config.test.json.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return {}; 3 | }; 4 | -------------------------------------------------------------------------------- /generators/app/templates/js/src/services/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | module.exports = function (app) { 3 | }; 4 | -------------------------------------------------------------------------------- /generators/middleware/templates/js/middleware.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return function <%= camelName %>(req, res, next) { 3 | next(); 4 | }; 5 | }; 6 | -------------------------------------------------------------------------------- /generators/app/templates/static/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/feathersjs-ecosystem/generator-feathers/HEAD/generators/app/templates/static/public/favicon.ico -------------------------------------------------------------------------------- /generators/service/templates/js/types/nedb.js: -------------------------------------------------------------------------------- 1 | const { Service } = require('feathers-nedb'); 2 | 3 | exports.<%= className %> = class <%= className %> extends Service { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /generators/service/templates/js/types/memory.js: -------------------------------------------------------------------------------- 1 | const { Service } = require('feathers-memory'); 2 | 3 | exports.<%= className %> = class <%= className %> extends Service { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /generators/service/templates/js/types/mongoose.js: -------------------------------------------------------------------------------- 1 | const { Service } = require('feathers-mongoose'); 2 | 3 | exports.<%= className %> = class <%= className %> extends Service { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /generators/service/templates/js/types/sequelize.js: -------------------------------------------------------------------------------- 1 | const { Service } = require('feathers-sequelize'); 2 | 3 | exports.<%= className %> = class <%= className %> extends Service { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /generators/app/templates/ts/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | globals: { 5 | 'ts-jest': { 6 | diagnostics: false 7 | } 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /generators/app/templates/js/src/middleware/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | module.exports = function (app) { 3 | // Add your custom middleware here. Remember that 4 | // in Express, the order matters. 5 | }; 6 | -------------------------------------------------------------------------------- /generators/app/templates/ts/src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../declarations'; 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | export default function (app: Application): void { 5 | } 6 | -------------------------------------------------------------------------------- /generators/app/configs/config.production.json.js: -------------------------------------------------------------------------------- 1 | module.exports = function(generator) { 2 | const { props } = generator; 3 | const config = { 4 | host: `${props.name}-app.feathersjs.com`, 5 | port: `PORT` 6 | }; 7 | 8 | return config; 9 | }; 10 | -------------------------------------------------------------------------------- /generators/connection/templates/js/knex.js: -------------------------------------------------------------------------------- 1 | const knex = require('knex'); 2 | 3 | module.exports = function (app) { 4 | const { client, connection } = app.get('<%= database %>'); 5 | const db = knex({ client, connection }); 6 | 7 | app.set('knexClient', db); 8 | }; 9 | -------------------------------------------------------------------------------- /generators/connection/templates/ts/couchbase.ts: -------------------------------------------------------------------------------- 1 | import couchbase from 'couchbase'; 2 | 3 | export default app => { 4 | const { host, options } = app.get('couchbase'); 5 | const cluster = new couchbase.Cluster(host, options); 6 | 7 | app.set('couchbaseCluster', cluster); 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = false 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /generators/connection/templates/js/couchbase.js: -------------------------------------------------------------------------------- 1 | const couchbase = require('couchbase'); 2 | 3 | module.exports = app => { 4 | const { host, options } = app.get('couchbase'); 5 | const cluster = new couchbase.Cluster(host, options); 6 | 7 | app.set('couchbaseCluster', cluster); 8 | }; 9 | -------------------------------------------------------------------------------- /generators/middleware/templates/ts/middleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | export default () => { 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 5 | return (req: Request, res: Response, next: NextFunction) => { 6 | next(); 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /generators/service/templates/js/types/knex.js: -------------------------------------------------------------------------------- 1 | const { Service } = require('feathers-knex'); 2 | 3 | exports.<%= className %> = class <%= className %> extends Service { 4 | constructor(options) { 5 | super({ 6 | ...options, 7 | name: '<%= snakeName %>' 8 | }); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /generators/service/templates/ts/test.jest.ts: -------------------------------------------------------------------------------- 1 | import app from '<%= relativeRoot %><%= libDirectory %>/app'; 2 | 3 | describe('\'<%= name %>\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('<%= path %>'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /generators/service/templates/js/test.jest.js: -------------------------------------------------------------------------------- 1 | const app = require('<%= relativeRoot %><%= libDirectory %>/app'); 2 | 3 | describe('\'<%= name %>\' service', () => { 4 | it('registered the service', () => { 5 | const service = app.service('<%= path %>'); 6 | expect(service).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /generators/app/templates/static/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /generators/connection/templates/ts/knex.ts: -------------------------------------------------------------------------------- 1 | import knex from 'knex'; 2 | import { Application } from './declarations'; 3 | 4 | export default function (app: Application): void { 5 | const { client, connection } = app.get('<%= database %>'); 6 | const db = knex({ client, connection }); 7 | 8 | app.set('knexClient', db); 9 | } 10 | -------------------------------------------------------------------------------- /generators/service/templates/js/types/prisma.js: -------------------------------------------------------------------------------- 1 | const { PrismaService } = require('feathers-prisma'); 2 | 3 | exports.<%= className %> = class <%= className %> extends PrismaService { 4 | constructor({ model, ...options }, app) { 5 | super({ 6 | model, 7 | ...options, 8 | }, app.get('prisma')); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /generators/service/templates/ts/types/cassandra.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'feathers-cassandra'; 2 | 3 | export class <%= className %> extends Service { 4 | constructor(options) { 5 | const { Model, ...otherOptions } = options; 6 | 7 | super({ 8 | ...otherOptions, 9 | model: Model 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /generators/app/configs/config.default.json.js: -------------------------------------------------------------------------------- 1 | module.exports = function(generator) { 2 | const { props } = generator; 3 | const config = { 4 | host: 'localhost', 5 | port: 3030, 6 | public: '../public/', 7 | paginate: { 8 | default: 10, 9 | max: 50 10 | } 11 | }; 12 | 13 | return config; 14 | }; 15 | -------------------------------------------------------------------------------- /generators/app/templates/ts/src/middleware/index.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../declarations'; 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function 5 | export default function (app: Application): void { 6 | } 7 | -------------------------------------------------------------------------------- /generators/hook/templates/js/hook.js: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | 4 | // eslint-disable-next-line no-unused-vars 5 | module.exports = (options = {}) => { 6 | return async context => { 7 | return context; 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /generators/app/configs/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | configDefault: require('./config.default.json.js'), 3 | configProduction: require('./config.production.json.js'), 4 | configTest: require('./config.test.json.js'), 5 | package: require('./package.json.js'), 6 | eslintrc: require('./eslintrc.json.js'), 7 | tsconfig: require('./tsconfig.json.js') 8 | }; 9 | -------------------------------------------------------------------------------- /generators/service/templates/js/model/nedb.js: -------------------------------------------------------------------------------- 1 | const NeDB = require('@seald-io/nedb'); 2 | const path = require('path'); 3 | 4 | module.exports = function (app) { 5 | const dbPath = app.get('nedb'); 6 | const Model = new NeDB({ 7 | filename: path.join(dbPath, '<%= kebabName %>.db'), 8 | autoload: true 9 | }); 10 | 11 | return Model; 12 | }; 13 | -------------------------------------------------------------------------------- /generators/app/templates/ts/src/declarations.d.ts: -------------------------------------------------------------------------------- 1 | import { Application as ExpressFeathers } from '@feathersjs/express'; 2 | 3 | // A mapping of service names to types. Will be extended in service files. 4 | export interface ServiceTypes {} 5 | // The application instance type that will be used everywhere else 6 | export type Application = ExpressFeathers; 7 | -------------------------------------------------------------------------------- /generators/connection/templates/js/mongoose.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const logger = require('./logger'); 3 | 4 | module.exports = function (app) { 5 | mongoose.connect( 6 | app.get('mongodb') 7 | ).catch(err => { 8 | logger.error(err); 9 | process.exit(1); 10 | }); 11 | 12 | app.set('mongooseClient', mongoose); 13 | }; 14 | -------------------------------------------------------------------------------- /generators/service/templates/js/types/cassandra.js: -------------------------------------------------------------------------------- 1 | const { Service } = require('feathers-cassandra'); 2 | 3 | exports.<%= className %> = class <%= className %> extends Service { 4 | constructor(options) { 5 | const { Model, ...otherOptions } = options; 6 | 7 | super({ 8 | ...otherOptions, 9 | model: Model 10 | }); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /generators/service/templates/js/types/objection.js: -------------------------------------------------------------------------------- 1 | const { Service } = require('feathers-objection'); 2 | 3 | exports.<%= className %> = class <%= className %> extends Service { 4 | constructor(options) { 5 | const { Model, ...otherOptions } = options; 6 | 7 | super({ 8 | ...otherOptions, 9 | model: Model 10 | }); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /generators/connection/templates/js/objection.js: -------------------------------------------------------------------------------- 1 | const { Model } = require('objection'); 2 | const knex = require('knex'); 3 | 4 | module.exports = function (app) { 5 | const { client, connection } = app.get('<%= database %>'); 6 | const db = knex({ client, connection, useNullAsDefault: false }); 7 | 8 | Model.knex(db); 9 | 10 | app.set('knex', db); 11 | }; 12 | -------------------------------------------------------------------------------- /generators/service/templates/ts/test.mocha.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import app from '<%= relativeRoot %><%= libDirectory %>/app'; 3 | 4 | describe('\'<%= name %>\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('<%= path %>'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /generators/service/templates/ts/types/couchbase.ts: -------------------------------------------------------------------------------- 1 | import { CouchbaseService } from 'feathers-couchbase'; 2 | 3 | export class <%= className %> extends CouchbaseService { 4 | constructor (options: any, app: any) { 5 | super({ 6 | cluster: app.get('couchbaseCluster'), 7 | name: '<%= camelName %>', 8 | ...options 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /generators/service/templates/js/types/mongodb.js: -------------------------------------------------------------------------------- 1 | const { Service } = require('feathers-mongodb'); 2 | 3 | exports.<%= className %> = class <%= className %> extends Service { 4 | constructor(options, app) { 5 | super(options); 6 | 7 | app.get('mongoClient').then(db => { 8 | this.Model = db.collection('<%= kebabName %>'); 9 | }); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /generators/service/templates/js/test.mocha.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('<%= relativeRoot %><%= libDirectory %>/app'); 3 | 4 | describe('\'<%= name %>\' service', () => { 5 | it('registered the service', () => { 6 | const service = app.service('<%= path %>'); 7 | 8 | assert.ok(service, 'Registered the service'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /generators/service/templates/js/types/couchbase.js: -------------------------------------------------------------------------------- 1 | const { CouchbaseService } = require('feathers-couchbase'); 2 | 3 | exports.<%= className %> = class <%= className %> extends CouchbaseService { 4 | constructor (options, app) { 5 | super({ 6 | cluster: app.get('couchbaseCluster'), 7 | name: '<%= camelName %>', 8 | ...options 9 | }); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /meta.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | app: 'Create a new Feathers application in the current folder', 3 | authentication: 'Set up authentication for the current application', 4 | connection: 'Initialize a new database connection', 5 | hook: 'Create a new hook', 6 | middleware: 'Create an Express middleware', 7 | service: 'Generate a new service', 8 | plugin: 'Create a new Feathers plugin' 9 | }; 10 | -------------------------------------------------------------------------------- /generators/service/templates/ts/types/nedb.ts: -------------------------------------------------------------------------------- 1 | import { Service, NedbServiceOptions } from 'feathers-nedb'; 2 | import { Application } from '<%= relativeRoot %>declarations'; 3 | 4 | export class <%= className %> extends Service { 5 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | constructor(options: Partial, app: Application) { 7 | super(options); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /generators/connection/templates/js/prisma.js: -------------------------------------------------------------------------------- 1 | const { PrismaClient } = require('@prisma/client'); 2 | 3 | module.exports = function (app) { 4 | const connectionString = app.get('<%= database %>'); 5 | const prisma = new PrismaClient({ 6 | datasources: { 7 | db: { 8 | url: connectionString, 9 | }, 10 | }, 11 | }); 12 | prisma.$connect(); 13 | app.set('prisma', prisma); 14 | }; 15 | -------------------------------------------------------------------------------- /generators/connection/templates/ts/mongoose.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import { Application } from './declarations'; 3 | import logger from './logger'; 4 | 5 | export default function (app: Application): void { 6 | mongoose.connect( 7 | app.get('mongodb') 8 | ).catch(err => { 9 | logger.error(err); 10 | process.exit(1); 11 | }); 12 | 13 | app.set('mongooseClient', mongoose); 14 | } 15 | -------------------------------------------------------------------------------- /generators/service/templates/ts/types/memory.ts: -------------------------------------------------------------------------------- 1 | import { Service, MemoryServiceOptions } from 'feathers-memory'; 2 | import { Application } from '<%= relativeRoot %>declarations'; 3 | 4 | export class <%= className %> extends Service { 5 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | constructor(options: Partial, app: Application) { 7 | super(options); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /generators/connection/templates/js/mongodb.js: -------------------------------------------------------------------------------- 1 | const MongoClient = require('mongodb').MongoClient; 2 | 3 | module.exports = function (app) { 4 | const connection = app.get('mongodb'); 5 | const database = connection.substr(connection.lastIndexOf('/') + 1); 6 | const mongoClient = MongoClient.connect(connection) 7 | .then(client => client.db(database)); 8 | 9 | app.set('mongoClient', mongoClient); 10 | }; 11 | -------------------------------------------------------------------------------- /generators/service/templates/ts/model/nedb.ts: -------------------------------------------------------------------------------- 1 | import NeDB from '@seald-io/nedb'; 2 | import path from 'path'; 3 | import { Application } from '../declarations'; 4 | 5 | export default function (app: Application): NeDB { 6 | const dbPath = app.get('nedb'); 7 | const Model = new NeDB({ 8 | filename: path.join(dbPath, '<%= kebabName %>.db'), 9 | autoload: true 10 | }); 11 | 12 | return Model; 13 | } 14 | -------------------------------------------------------------------------------- /generators/app/configs/tsconfig.json.js: -------------------------------------------------------------------------------- 1 | module.exports = function(generator) { 2 | const { props } = generator; 3 | const config = { 4 | compilerOptions: { 5 | target: 'es2018', 6 | module: 'commonjs', 7 | outDir: './lib', 8 | rootDir: `./${props.src}`, 9 | strict: true, 10 | esModuleInterop: true 11 | }, 12 | exclude: ['test'] 13 | }; 14 | 15 | return config; 16 | }; 17 | -------------------------------------------------------------------------------- /generators/connection/templates/ts/objection.ts: -------------------------------------------------------------------------------- 1 | import { Model } from 'objection'; 2 | import knex from 'knex'; 3 | import { Application } from './declarations'; 4 | 5 | export default function (app: Application): void { 6 | const { client, connection } = app.get('<%= database %>'); 7 | const db = knex({ client, connection, useNullAsDefault: false }); 8 | 9 | Model.knex(db); 10 | 11 | app.set('knex', db); 12 | } 13 | -------------------------------------------------------------------------------- /generators/service/templates/ts/types/mongoose.ts: -------------------------------------------------------------------------------- 1 | import { Service, MongooseServiceOptions } from 'feathers-mongoose'; 2 | import { Application } from '<%= relativeRoot %>declarations'; 3 | 4 | export class <%= className %> extends Service { 5 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | constructor(options: Partial, app: Application) { 7 | super(options); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /generators/service/templates/js/model/nedb-user.js: -------------------------------------------------------------------------------- 1 | const NeDB = require('@seald-io/nedb'); 2 | const path = require('path'); 3 | 4 | module.exports = function (app) { 5 | const dbPath = app.get('nedb'); 6 | const Model = new NeDB({ 7 | filename: path.join(dbPath, '<%= kebabName %>.db'), 8 | autoload: true 9 | }); 10 | 11 | Model.ensureIndex({ fieldName: 'email', unique: true }); 12 | 13 | return Model; 14 | }; 15 | -------------------------------------------------------------------------------- /generators/service/templates/ts/types/sequelize.ts: -------------------------------------------------------------------------------- 1 | import { Service, SequelizeServiceOptions } from 'feathers-sequelize'; 2 | import { Application } from '<%= relativeRoot %>declarations'; 3 | 4 | export class <%= className %> extends Service { 5 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | constructor(options: Partial, app: Application) { 7 | super(options); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /generators/app/templates/ts/src/index.ts: -------------------------------------------------------------------------------- 1 | import logger from './logger'; 2 | import app from './app'; 3 | 4 | const port = app.get('port'); 5 | const server = app.listen(port); 6 | 7 | process.on('unhandledRejection', (reason, p) => 8 | logger.error('Unhandled Rejection at: Promise ', p, reason) 9 | ); 10 | 11 | server.on('listening', () => 12 | logger.info('Feathers application started on http://%s:%d', app.get('host'), port) 13 | ); 14 | -------------------------------------------------------------------------------- /generators/hook/templates/ts/hook.ts: -------------------------------------------------------------------------------- 1 | // Use this hook to manipulate incoming or outgoing data. 2 | // For more information on hooks see: http://docs.feathersjs.com/api/hooks.html 3 | import { Hook, HookContext } from '@feathersjs/feathers'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | export default (options = {}): Hook => { 7 | return async (context: HookContext): Promise => { 8 | return context; 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /generators/connection/templates/ts/mongodb.ts: -------------------------------------------------------------------------------- 1 | import { MongoClient } from 'mongodb'; 2 | import { Application } from './declarations'; 3 | 4 | export default function (app: Application): void { 5 | const connection = app.get('mongodb'); 6 | const database = connection.substr(connection.lastIndexOf('/') + 1); 7 | const mongoClient = MongoClient.connect(connection) 8 | .then(client => client.db(database)); 9 | 10 | app.set('mongoClient', mongoClient); 11 | } 12 | -------------------------------------------------------------------------------- /generators/connection/templates/ts/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import { Application } from './declarations'; 3 | 4 | export default function (app: Application): void { 5 | const connectionString = app.get('<%= database %>'); 6 | const prisma = new PrismaClient({ 7 | datasources: { 8 | db: { 9 | url: connectionString, 10 | }, 11 | }, 12 | }); 13 | prisma.$connect(); 14 | app.set('prisma', prisma); 15 | } 16 | -------------------------------------------------------------------------------- /generators/service/templates/ts/types/knex.ts: -------------------------------------------------------------------------------- 1 | import { Service, KnexServiceOptions } from 'feathers-knex'; 2 | import { Application } from '<%= relativeRoot %>declarations'; 3 | 4 | export class <%= className %> extends Service { 5 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | constructor(options: Partial, app: Application) { 7 | super({ 8 | ...options, 9 | name: '<%= snakeName %>' 10 | }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /generators/app/templates/js/src/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const logger = require('./logger'); 3 | const app = require('./app'); 4 | const port = app.get('port'); 5 | const server = app.listen(port); 6 | 7 | process.on('unhandledRejection', (reason, p) => 8 | logger.error('Unhandled Rejection at: Promise ', p, reason) 9 | ); 10 | 11 | server.on('listening', () => 12 | logger.info('Feathers application started on http://%s:%d', app.get('host'), port) 13 | ); 14 | -------------------------------------------------------------------------------- /generators/service/templates/ts/types/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaService, PrismaServiceOptions } from 'feathers-prisma'; 2 | import { Application } from '<%= relativeRoot %>declarations'; 3 | 4 | interface Options extends PrismaServiceOptions {} 5 | 6 | export class <%= className %> extends PrismaService { 7 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 8 | constructor(options: Options, app: Application) { 9 | super(options, app.get('prisma')); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /generators/service/templates/ts/model/nedb-user.ts: -------------------------------------------------------------------------------- 1 | import NeDB from '@seald-io/nedb'; 2 | import path from 'path'; 3 | import { Application } from '../declarations'; 4 | 5 | export default function (app: Application): NeDB { 6 | const dbPath = app.get('nedb'); 7 | const Model = new NeDB({ 8 | filename: path.join(dbPath, '<%= kebabName %>.db'), 9 | autoload: true 10 | }); 11 | 12 | Model.ensureIndex({ fieldName: 'email', unique: true }); 13 | 14 | return Model; 15 | } 16 | -------------------------------------------------------------------------------- /generators/app/templates/ts/src/logger.ts: -------------------------------------------------------------------------------- 1 | import { createLogger, format, transports } from 'winston'; 2 | 3 | // Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston 4 | const logger = createLogger({ 5 | // To see more detailed errors, change this to 'debug' 6 | level: 'info', 7 | format: format.combine( 8 | format.splat(), 9 | format.simple() 10 | ), 11 | transports: [ 12 | new transports.Console() 13 | ], 14 | }); 15 | 16 | export default logger; 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true, 5 | "mocha": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 2018 9 | }, 10 | "extends": "eslint:recommended", 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 2 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "single" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /generators/app/templates/js/src/logger.js: -------------------------------------------------------------------------------- 1 | const { createLogger, format, transports } = require('winston'); 2 | 3 | // Configure the Winston logger. For the complete documentation see https://github.com/winstonjs/winston 4 | const logger = createLogger({ 5 | // To see more detailed errors, change this to 'debug' 6 | level: 'info', 7 | format: format.combine( 8 | format.splat(), 9 | format.simple() 10 | ), 11 | transports: [ 12 | new transports.Console() 13 | ], 14 | }); 15 | 16 | module.exports = logger; 17 | -------------------------------------------------------------------------------- /generators/upgrade/templates/js/authentication.js: -------------------------------------------------------------------------------- 1 | const { AuthenticationService, JWTStrategy } = require('@feathersjs/authentication'); 2 | const { LocalStrategy } = require('@feathersjs/authentication-local'); 3 | const { expressOauth } = require('@feathersjs/authentication-oauth'); 4 | 5 | module.exports = app => { 6 | const authentication = new AuthenticationService(app); 7 | 8 | authentication.register('jwt', new JWTStrategy()); 9 | authentication.register('local', new LocalStrategy()); 10 | 11 | app.use('/authentication', authentication); 12 | app.configure(expressOauth()); 13 | }; 14 | -------------------------------------------------------------------------------- /generators/app/templates/js/src/app.hooks.js: -------------------------------------------------------------------------------- 1 | // Application hooks that run for every service 2 | 3 | module.exports = { 4 | before: { 5 | all: [], 6 | find: [], 7 | get: [], 8 | create: [], 9 | update: [], 10 | patch: [], 11 | remove: [] 12 | }, 13 | 14 | after: { 15 | all: [], 16 | find: [], 17 | get: [], 18 | create: [], 19 | update: [], 20 | patch: [], 21 | remove: [] 22 | }, 23 | 24 | error: { 25 | all: [], 26 | find: [], 27 | get: [], 28 | create: [], 29 | update: [], 30 | patch: [], 31 | remove: [] 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /generators/service/templates/ts/types/objection.ts: -------------------------------------------------------------------------------- 1 | import { Service, ObjectionServiceOptions } from 'feathers-objection'; 2 | import { Application } from '<%= relativeRoot %>declarations'; 3 | 4 | interface Options extends ObjectionServiceOptions { 5 | Model: any; 6 | } 7 | 8 | export class <%= className %> extends Service { 9 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 10 | constructor(options: Partial, app: Application) { 11 | const { Model, ...otherOptions } = options; 12 | 13 | super({ 14 | ...otherOptions, 15 | model: Model 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /generators/service/templates/ts/types/mongodb.ts: -------------------------------------------------------------------------------- 1 | import { Db } from 'mongodb'; 2 | import { Service, MongoDBServiceOptions } from 'feathers-mongodb'; 3 | import { Application } from '<%= relativeRoot %>declarations'; 4 | 5 | export class <%= className %> extends Service { 6 | //eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | constructor(options: Partial, app: Application) { 8 | super(options); 9 | 10 | const client: Promise = app.get('mongoClient'); 11 | 12 | client.then(db => { 13 | this.Model = db.collection('<%= kebabName %>'); 14 | }); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /generators/upgrade/templates/ts/authentication.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../../declarations'; 2 | import { AuthenticationService, JWTStrategy } from '@feathersjs/authentication'; 3 | import { LocalStrategy } from '@feathersjs/authentication-local'; 4 | import { expressOauth } from '@feathersjs/authentication-oauth'; 5 | 6 | export default function(app: Application): void { 7 | const authentication = new AuthenticationService(app); 8 | 9 | authentication.register('jwt', new JWTStrategy()); 10 | authentication.register('local', new LocalStrategy()); 11 | 12 | app.use('/authentication', authentication); 13 | app.configure(expressOauth()); 14 | } 15 | -------------------------------------------------------------------------------- /generators/app/templates/ts/src/app.hooks.ts: -------------------------------------------------------------------------------- 1 | // Application hooks that run for every service 2 | // Don't remove this comment. It's needed to format import lines nicely. 3 | 4 | export default { 5 | before: { 6 | all: [], 7 | find: [], 8 | get: [], 9 | create: [], 10 | update: [], 11 | patch: [], 12 | remove: [] 13 | }, 14 | 15 | after: { 16 | all: [], 17 | find: [], 18 | get: [], 19 | create: [], 20 | update: [], 21 | patch: [], 22 | remove: [] 23 | }, 24 | 25 | error: { 26 | all: [], 27 | find: [], 28 | get: [], 29 | create: [], 30 | update: [], 31 | patch: [], 32 | remove: [] 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /generators/authentication/templates/js/authentication.js: -------------------------------------------------------------------------------- 1 | const { AuthenticationService, JWTStrategy } = require('@feathersjs/authentication'); 2 | <% if(strategies.includes('local')) { %>const { LocalStrategy } = require('@feathersjs/authentication-local');<% } %> 3 | const { expressOauth } = require('@feathersjs/authentication-oauth'); 4 | 5 | module.exports = app => { 6 | const authentication = new AuthenticationService(app); 7 | 8 | authentication.register('jwt', new JWTStrategy()); 9 | <% if(strategies.includes('local')) { %> authentication.register('local', new LocalStrategy());<% } %> 10 | 11 | app.use('/authentication', authentication); 12 | app.configure(expressOauth()); 13 | }; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /generators/service/templates/js/hooks.js: -------------------------------------------------------------------------------- 1 | <% if (requiresAuth) { %>const { authenticate } = require('@feathersjs/authentication').hooks;<% } %> 2 | 3 | module.exports = { 4 | before: { 5 | all: [<% if (requiresAuth) { %> authenticate('jwt') <% } %>], 6 | find: [], 7 | get: [], 8 | create: [], 9 | update: [], 10 | patch: [], 11 | remove: [] 12 | }, 13 | 14 | after: { 15 | all: [], 16 | find: [], 17 | get: [], 18 | create: [], 19 | update: [], 20 | patch: [], 21 | remove: [] 22 | }, 23 | 24 | error: { 25 | all: [], 26 | find: [], 27 | get: [], 28 | create: [], 29 | update: [], 30 | patch: [], 31 | remove: [] 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /generators/service/templates/js/model/knex.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | // <%= name %>-model.js - A KnexJS 4 | // 5 | // See http://knexjs.org/ 6 | // for more of what you can do here. 7 | module.exports = function (app) { 8 | const db = app.get('knexClient'); 9 | const tableName = '<%= snakeName %>'; 10 | db.schema.hasTable(tableName).then(exists => { 11 | if(!exists) { 12 | db.schema.createTable(tableName, table => { 13 | table.increments('id'); 14 | table.string('text'); 15 | }) 16 | .then(() => console.log(`Created ${tableName} table`)) 17 | .catch(e => console.error(`Error creating ${tableName} table`, e)); 18 | } 19 | }); 20 | 21 | 22 | return db; 23 | }; 24 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | language: ["js", "ts"] 13 | tester: ["mocha", "jest"] 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: 14 20 | - name: Start MongoDB 21 | uses: supercharge/mongodb-github-action@1.3.0 22 | - run: npm ci 23 | - run: npm test 24 | if: ${{ !(matrix.language == 'ts' && matrix.tester == 'jest') }} 25 | env: 26 | GENERATOR_LANGUAGE: ${{matrix.language}} 27 | GENERATOR_TESTER: ${{matrix.tester}} 28 | -------------------------------------------------------------------------------- /generators/app/templates/static/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | data/ 31 | -------------------------------------------------------------------------------- /generators/service/templates/ts/model/knex.ts: -------------------------------------------------------------------------------- 1 | // <%= name %>-model.ts - A KnexJS 2 | // 3 | // See http://knexjs.org/ 4 | // for more of what you can do here. 5 | import { Knex } from 'knex'; 6 | import { Application } from '../declarations'; 7 | 8 | export default function (app: Application): Knex { 9 | const db: Knex = app.get('knexClient'); 10 | const tableName = '<%= snakeName %>'; 11 | db.schema.hasTable(tableName).then(exists => { 12 | if(!exists) { 13 | db.schema.createTable(tableName, table => { 14 | table.increments('id'); 15 | table.string('text'); 16 | }) 17 | .then(() => console.log(`Created ${tableName} table`)) 18 | .catch(e => console.error(`Error creating ${tableName} table`, e)); 19 | } 20 | }); 21 | 22 | 23 | return db; 24 | } 25 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | const cp = require('child_process'); 2 | 3 | // Start a process and wait either for it to exit 4 | // or to display a certain text 5 | exports.startAndWait = (cmd, args, options, text) => { 6 | return new Promise((resolve, reject) => { 7 | let buffer = ''; 8 | 9 | const child = cp.spawn(cmd, args, options); 10 | const addToBuffer = data => { 11 | buffer += data; 12 | 13 | if(text && buffer.indexOf(text) !== -1) { 14 | resolve({ buffer, child }); 15 | } 16 | }; 17 | 18 | child.stdout.on('data', addToBuffer); 19 | child.stderr.on('data', addToBuffer); 20 | 21 | child.on('exit', status => { 22 | if(status !== 0) { 23 | return reject(new Error(buffer)); 24 | } 25 | 26 | resolve({ buffer, child }); 27 | }); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /generators/connection/templates/js/cassandra.js: -------------------------------------------------------------------------------- 1 | const ExpressCassandra = require('express-cassandra'); 2 | const FeathersCassandra = require('feathers-cassandra'); 3 | const Cassanknex = require('cassanknex'); 4 | 5 | module.exports = function (app) { 6 | const connectionInfo = app.get('<%= database %>'); 7 | const models = ExpressCassandra.createClient(connectionInfo); 8 | const cassandraClient = models.orm.get_system_client(); 9 | 10 | app.set('models', models); 11 | 12 | cassandraClient.connect(err => { 13 | if (err) throw err; 14 | 15 | const cassanknex = Cassanknex({ connection: cassandraClient }); 16 | 17 | FeathersCassandra.cassanknex(cassanknex); 18 | 19 | cassanknex.on('ready', err => { 20 | if (err) throw err; 21 | }); 22 | 23 | app.set('cassanknex', cassanknex); 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /generators/service/templates/js/types/generic.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | exports.<%= className %> = class <%= className %> { 3 | constructor (options) { 4 | this.options = options || {}; 5 | } 6 | 7 | async find (params) { 8 | return []; 9 | } 10 | 11 | async get (id, params) { 12 | return { 13 | id, text: `A new message with ID: ${id}!` 14 | }; 15 | } 16 | 17 | async create (data, params) { 18 | if (Array.isArray(data)) { 19 | return Promise.all(data.map(current => this.create(current, params))); 20 | } 21 | 22 | return data; 23 | } 24 | 25 | async update (id, data, params) { 26 | return data; 27 | } 28 | 29 | async patch (id, data, params) { 30 | return data; 31 | } 32 | 33 | async remove (id, params) { 34 | return { id }; 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /generators/service/templates/js/model/mongoose.js: -------------------------------------------------------------------------------- 1 | // <%= name %>-model.js - A mongoose model 2 | // 3 | // See http://mongoosejs.com/docs/models.html 4 | // for more of what you can do here. 5 | module.exports = function (app) { 6 | const modelName = '<%= camelName %>'; 7 | const mongooseClient = app.get('mongooseClient'); 8 | const { Schema } = mongooseClient; 9 | const schema = new Schema({ 10 | text: { type: String, required: true } 11 | }, { 12 | timestamps: true 13 | }); 14 | 15 | // This is necessary to avoid model compilation errors in watch mode 16 | // see https://mongoosejs.com/docs/api/connection.html#connection_Connection-deleteModel 17 | if (mongooseClient.modelNames().includes(modelName)) { 18 | mongooseClient.deleteModel(modelName); 19 | } 20 | return mongooseClient.model(modelName, schema); 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /generators/authentication/templates/ts/authentication.ts: -------------------------------------------------------------------------------- 1 | import { ServiceAddons } from '@feathersjs/feathers'; 2 | import { AuthenticationService, JWTStrategy } from '@feathersjs/authentication'; 3 | import { LocalStrategy } from '@feathersjs/authentication-local'; 4 | import { expressOauth } from '@feathersjs/authentication-oauth'; 5 | 6 | import { Application } from './declarations'; 7 | 8 | declare module './declarations' { 9 | interface ServiceTypes { 10 | 'authentication': AuthenticationService & ServiceAddons; 11 | } 12 | } 13 | 14 | export default function(app: Application): void { 15 | const authentication = new AuthenticationService(app); 16 | 17 | authentication.register('jwt', new JWTStrategy()); 18 | authentication.register('local', new LocalStrategy()); 19 | 20 | app.use('/authentication', authentication); 21 | app.configure(expressOauth()); 22 | } 23 | -------------------------------------------------------------------------------- /generators/service/templates/js/model/sequelize.js: -------------------------------------------------------------------------------- 1 | // See https://sequelize.org/master/manual/model-basics.html 2 | // for more of what you can do here. 3 | const Sequelize = require('sequelize'); 4 | const DataTypes = Sequelize.DataTypes; 5 | 6 | module.exports = function (app) { 7 | const sequelizeClient = app.get('sequelizeClient'); 8 | const <%= camelName %> = sequelizeClient.define('<%= snakeName %>', { 9 | text: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | } 13 | }, { 14 | hooks: { 15 | beforeCount(options) { 16 | options.raw = true; 17 | } 18 | } 19 | }); 20 | 21 | // eslint-disable-next-line no-unused-vars 22 | <%= camelName %>.associate = function (models) { 23 | // Define associations here 24 | // See https://sequelize.org/master/manual/assocs.html 25 | }; 26 | 27 | return <%= camelName %>; 28 | }; 29 | -------------------------------------------------------------------------------- /generators/service/templates/ts/hooks.ts: -------------------------------------------------------------------------------- 1 | import { HooksObject } from '@feathersjs/feathers'; 2 | <% if (requiresAuth) { %>import * as authentication from '@feathersjs/authentication'; 3 | // Don't remove this comment. It's needed to format import lines nicely. 4 | 5 | const { authenticate } = authentication.hooks; 6 | <% } %> 7 | export default { 8 | before: { 9 | all: [<% if (requiresAuth) { %> authenticate('jwt') <% } %>], 10 | find: [], 11 | get: [], 12 | create: [], 13 | update: [], 14 | patch: [], 15 | remove: [] 16 | }, 17 | 18 | after: { 19 | all: [], 20 | find: [], 21 | get: [], 22 | create: [], 23 | update: [], 24 | patch: [], 25 | remove: [] 26 | }, 27 | 28 | error: { 29 | all: [], 30 | find: [], 31 | get: [], 32 | create: [], 33 | update: [], 34 | patch: [], 35 | remove: [] 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /generators/connection/templates/ts/cassandra.ts: -------------------------------------------------------------------------------- 1 | import ExpressCassandra from 'express-cassandra'; 2 | import FeathersCassandra from 'feathers-cassandra'; 3 | import Cassanknex from 'cassanknex'; 4 | import { Application } from './declarations'; 5 | 6 | export default function (app: Application): void { 7 | const connectionInfo = app.get('<%= database %>'); 8 | const models = ExpressCassandra.createClient(connectionInfo); 9 | const cassandraClient = models.orm.get_system_client(); 10 | 11 | app.set('models', models); 12 | 13 | cassandraClient.connect((err: any) => { 14 | if (err) throw err; 15 | 16 | const cassanknex = Cassanknex({ connection: cassandraClient }); 17 | 18 | FeathersCassandra.cassanknex(cassanknex); 19 | 20 | cassanknex.on('ready', (err: any) => { 21 | if (err) throw err; 22 | }); 23 | 24 | app.set('cassanknex', cassanknex); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /generators/connection/templates/js/sequelize.js: -------------------------------------------------------------------------------- 1 | const Sequelize = require('sequelize'); 2 | 3 | module.exports = function (app) { 4 | const connectionString = app.get('<%= database %>'); 5 | const sequelize = new Sequelize(connectionString, { 6 | dialect: '<%= database %>', 7 | logging: false, 8 | define: { 9 | freezeTableName: true 10 | } 11 | }); 12 | const oldSetup = app.setup; 13 | 14 | app.set('sequelizeClient', sequelize); 15 | 16 | app.setup = function (...args) { 17 | const result = oldSetup.apply(this, args); 18 | 19 | // Set up data relationships 20 | const models = sequelize.models; 21 | Object.keys(models).forEach(name => { 22 | if ('associate' in models[name]) { 23 | models[name].associate(models); 24 | } 25 | }); 26 | 27 | // Sync to the database 28 | app.set('sequelizeSync', sequelize.sync()); 29 | 30 | return result; 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /generators/service/templates/js/service.js: -------------------------------------------------------------------------------- 1 | // Initializes the `<%= name %>` service on path `/<%= path %>` 2 | const { <%= className %> } = require('./<%= kebabName %>.class');<% if(modelName) { %> 3 | const createModel = require('<%= relativeRoot %>models/<%= modelName %>');<% } %> 4 | const hooks = require('./<%= kebabName %>.hooks'); 5 | 6 | module.exports = function (app) { 7 | const options = {<% if (modelName) { %> 8 | Model: createModel(app),<% } %><% if (serviceModule === 'feathers-prisma') {%> 9 | model: '<%= className[0].toLowerCase() + className.substr(1) %>', 10 | client: app.get('prisma'),<% } %> 11 | paginate: app.get('paginate') 12 | }; 13 | 14 | // Initialize our service with any options it requires 15 | app.use('/<%= path %>', new <%= className %>(options, app)); 16 | 17 | // Get our initialized service so that we can register hooks 18 | const service = app.service('<%= path %>'); 19 | 20 | service.hooks(hooks); 21 | }; 22 | -------------------------------------------------------------------------------- /generators/service/templates/js/model/cassandra.js: -------------------------------------------------------------------------------- 1 | // See https://express-cassandra.readthedocs.io/en/latest/schema/ 2 | // for more of what you can do here. 3 | module.exports = function (app) { 4 | const models = app.get('models'); 5 | const <%= camelName %> = models.loadSchema('<%= camelName %>', { 6 | table_name: '<%= snakeName %>', 7 | fields: { 8 | id: 'int', 9 | text: { 10 | type: 'text', 11 | rule: { 12 | required: true 13 | } 14 | } 15 | }, 16 | key: ['id'], 17 | custom_indexes: [ 18 | { 19 | on: 'text', 20 | using: 'org.apache.cassandra.index.sasi.SASIIndex', 21 | options: {} 22 | } 23 | ], 24 | options: { 25 | timestamps: true 26 | } 27 | }, function (err) { 28 | if (err) throw err; 29 | }); 30 | 31 | <%= camelName %>.syncDB(function (err) { 32 | if (err) throw err; 33 | }); 34 | 35 | return <%= camelName %>; 36 | }; 37 | -------------------------------------------------------------------------------- /generators/service/templates/ts/model/mongoose.ts: -------------------------------------------------------------------------------- 1 | // <%= name %>-model.ts - A mongoose model 2 | // 3 | // See http://mongoosejs.com/docs/models.html 4 | // for more of what you can do here. 5 | import { Application } from '../declarations'; 6 | import { Model, Mongoose } from 'mongoose'; 7 | 8 | export default function (app: Application): Model { 9 | const modelName = '<%= camelName %>'; 10 | const mongooseClient: Mongoose = app.get('mongooseClient'); 11 | const { Schema } = mongooseClient; 12 | const schema = new Schema({ 13 | text: { type: String, required: true } 14 | }, { 15 | timestamps: true 16 | }); 17 | 18 | // This is necessary to avoid model compilation errors in watch mode 19 | // see https://mongoosejs.com/docs/api/connection.html#connection_Connection-deleteModel 20 | if (mongooseClient.modelNames().includes(modelName)) { 21 | (mongooseClient as any).deleteModel(modelName); 22 | } 23 | return mongooseClient.model(modelName, schema); 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/update-dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Update dependencies 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 1 * *' 6 | workflow_dispatch: 7 | jobs: 8 | update-dependencies: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Use Node.js 13 | uses: actions/setup-node@v1 14 | with: 15 | node-version: 14 16 | - run: npm ci 17 | - run: | 18 | git config user.name "GitHub Actions Bot" 19 | git config user.email "hello@feathersjs.com" 20 | git checkout -b update-dependencies-$GITHUB_RUN_ID 21 | - run: | 22 | npm run update-dependencies 23 | npm install 24 | - run: | 25 | git commit -am "chore(dependencies): Update dependencies" 26 | git push origin update-dependencies-$GITHUB_RUN_ID 27 | - run: | 28 | gh pr create --title "chore(dependencies): Update all dependencies" --body "" 29 | env: 30 | GITHUB_TOKEN: ${{secrets.CI_ACCESS_TOKEN}} 31 | -------------------------------------------------------------------------------- /generators/service/templates/js/model/knex-user.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | // <%= name %>-model.js - A KnexJS 4 | // 5 | // See http://knexjs.org/ 6 | // for more of what you can do here. 7 | module.exports = function (app) { 8 | const db = app.get('knexClient'); 9 | const tableName = '<%= snakeName %>'; 10 | 11 | db.schema.hasTable(tableName).then(exists => { 12 | if(!exists) { 13 | db.schema.createTable(tableName, table => { 14 | table.increments('id'); 15 | <% if(authentication.strategies.indexOf('local') !== -1) { %> 16 | table.string('email').unique(); 17 | table.string('password'); 18 | <% } %> 19 | <% authentication.oauthProviders.forEach(provider => { %> 20 | table.string('<%= provider %>Id'); 21 | <% }); %> 22 | }) 23 | .then(() => console.log(`Created ${tableName} table`)) 24 | .catch(e => console.error(`Error creating ${tableName} table`, e)); 25 | } 26 | }); 27 | 28 | return db; 29 | }; 30 | -------------------------------------------------------------------------------- /generators/connection/templates/ts/sequelize.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize } from 'sequelize'; 2 | import { Application } from './declarations'; 3 | 4 | export default function (app: Application): void { 5 | const connectionString = app.get('<%= database %>'); 6 | const sequelize = new Sequelize(connectionString, { 7 | dialect: '<%= database %>', 8 | logging: false, 9 | define: { 10 | freezeTableName: true 11 | } 12 | }); 13 | const oldSetup = app.setup; 14 | 15 | app.set('sequelizeClient', sequelize); 16 | 17 | app.setup = function (...args): Application { 18 | const result = oldSetup.apply(this, args); 19 | 20 | // Set up data relationships 21 | const models = sequelize.models; 22 | Object.keys(models).forEach(name => { 23 | if ('associate' in models[name]) { 24 | (models[name] as any).associate(models); 25 | } 26 | }); 27 | 28 | // Sync to the database 29 | app.set('sequelizeSync', sequelize.sync()); 30 | 31 | return result; 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /generators/service/templates/ts/model/cassandra.ts: -------------------------------------------------------------------------------- 1 | // See https://express-cassandra.readthedocs.io/en/latest/schema/ 2 | // for more of what you can do here. 3 | import { Application } from '../declarations'; 4 | 5 | export default function (app: Application): any { 6 | const models = app.get('models'); 7 | const <%= camelName %> = models.loadSchema('<%= camelName %>', { 8 | table_name: '<%= snakeName %>', 9 | fields: { 10 | id: 'int', 11 | text: { 12 | type: 'text', 13 | rule: { 14 | required: true 15 | } 16 | } 17 | }, 18 | key: ['id'], 19 | custom_indexes: [ 20 | { 21 | on: 'text', 22 | using: 'org.apache.cassandra.index.sasi.SASIIndex', 23 | options: {} 24 | } 25 | ], 26 | options: { 27 | timestamps: true 28 | } 29 | }, function (err: any) { 30 | if (err) throw err; 31 | }); 32 | 33 | <%= camelName %>.syncDB(function (err: any) { 34 | if (err) throw err; 35 | }); 36 | 37 | return <%= camelName %>; 38 | } 39 | -------------------------------------------------------------------------------- /generators/service/templates/ts/model/sequelize.ts: -------------------------------------------------------------------------------- 1 | // See https://sequelize.org/master/manual/model-basics.html 2 | // for more of what you can do here. 3 | import { Sequelize, DataTypes, Model } from 'sequelize'; 4 | import { Application } from '../declarations'; 5 | import { HookReturn } from 'sequelize/types/hooks'; 6 | 7 | export default function (app: Application): typeof Model { 8 | const sequelizeClient: Sequelize = app.get('sequelizeClient'); 9 | const <%= camelName %> = sequelizeClient.define('<%= snakeName %>', { 10 | text: { 11 | type: DataTypes.STRING, 12 | allowNull: false 13 | } 14 | }, { 15 | hooks: { 16 | beforeCount(options: any): HookReturn { 17 | options.raw = true; 18 | } 19 | } 20 | }); 21 | 22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 | (<%= camelName %> as any).associate = function (models: any): void { 24 | // Define associations here 25 | // See https://sequelize.org/master/manual/assocs.html 26 | }; 27 | 28 | return <%= camelName %>; 29 | } 30 | -------------------------------------------------------------------------------- /generators/authentication/templates/js/test.jest.js: -------------------------------------------------------------------------------- 1 | const app = require('../<%= libDirectory %>/app'); 2 | 3 | describe('authentication', () => { 4 | it('registered the authentication service', () => { 5 | expect(app.service('authentication')).toBeTruthy(); 6 | }); 7 | <% if(strategies.includes('local')) { %> 8 | describe('local strategy', () => { 9 | const userInfo = { 10 | email: 'someone@example.com', 11 | password: 'supersecret' 12 | }; 13 | 14 | beforeAll(async () => { 15 | try { 16 | await app.service('users').create(userInfo); 17 | } catch (error) { 18 | // Do nothing, it just means the user already exists and can be tested 19 | } 20 | }); 21 | 22 | it('authenticates user and creates accessToken', async () => { 23 | const { user, accessToken } = await app.service('authentication').create({ 24 | strategy: 'local', 25 | ...userInfo 26 | }); 27 | 28 | expect(accessToken).toBeTruthy(); 29 | expect(user).toBeTruthy(); 30 | }); 31 | });<% } %> 32 | }); 33 | -------------------------------------------------------------------------------- /generators/authentication/templates/ts/test.jest.ts: -------------------------------------------------------------------------------- 1 | import app from '../<%= libDirectory %>/app'; 2 | 3 | describe('authentication', () => { 4 | it('registered the authentication service', () => { 5 | expect(app.service('authentication')).toBeTruthy(); 6 | }); 7 | <% if(strategies.includes('local')) { %> 8 | describe('local strategy', () => { 9 | const userInfo = { 10 | email: 'someone@example.com', 11 | password: 'supersecret' 12 | }; 13 | 14 | beforeAll(async () => { 15 | try { 16 | await app.service('users').create(userInfo); 17 | } catch (error) { 18 | // Do nothing, it just means the user already exists and can be tested 19 | } 20 | }); 21 | 22 | it('authenticates user and creates accessToken', async () => { 23 | const { user, accessToken } = await app.service('authentication').create({ 24 | strategy: 'local', 25 | ...userInfo 26 | }, {}); 27 | 28 | expect(accessToken).toBeTruthy(); 29 | expect(user).toBeTruthy(); 30 | }); 31 | });<% } %> 32 | }); 33 | -------------------------------------------------------------------------------- /generators/service/templates/ts/model/knex-user.ts: -------------------------------------------------------------------------------- 1 | // <%= name %>-model.ts - A KnexJS 2 | // 3 | // See http://knexjs.org/ 4 | // for more of what you can do here. 5 | import { Application } from '../declarations'; 6 | import { Knex } from 'knex'; 7 | 8 | export default function (app: Application): Knex { 9 | const db: Knex = app.get('knexClient'); 10 | const tableName = '<%= snakeName %>'; 11 | 12 | db.schema.hasTable(tableName).then(exists => { 13 | if(!exists) { 14 | db.schema.createTable(tableName, table => { 15 | table.increments('id'); 16 | <% if(authentication.strategies.indexOf('local') !== -1) { %> 17 | table.string('email').unique(); 18 | table.string('password'); 19 | <% } %> 20 | <% authentication.oauthProviders.forEach(provider => { %> 21 | table.string('<%= provider %>Id'); 22 | <% }); %> 23 | }) 24 | .then(() => console.log(`Created ${tableName} table`)) 25 | .catch(e => console.error(`Error creating ${tableName} table`, e)); 26 | } 27 | }); 28 | 29 | return db; 30 | } 31 | -------------------------------------------------------------------------------- /generators/service/templates/js/model/mongoose-user.js: -------------------------------------------------------------------------------- 1 | // <%= name %>-model.js - A mongoose model 2 | // 3 | // See http://mongoosejs.com/docs/models.html 4 | // for more of what you can do here. 5 | module.exports = function (app) { 6 | const modelName = '<%= camelName %>'; 7 | const mongooseClient = app.get('mongooseClient'); 8 | const schema = new mongooseClient.Schema({ 9 | <% if(authentication.strategies.indexOf('local') !== -1) { %> 10 | email: { type: String, unique: true, lowercase: true }, 11 | password: { type: String }, 12 | <% } %> 13 | <% authentication.oauthProviders.forEach(provider => { %> 14 | <%= provider %>Id: { type: String }, 15 | <% }); %> 16 | }, { 17 | timestamps: true 18 | }); 19 | 20 | // This is necessary to avoid model compilation errors in watch mode 21 | // see https://mongoosejs.com/docs/api/connection.html#connection_Connection-deleteModel 22 | if (mongooseClient.modelNames().includes(modelName)) { 23 | mongooseClient.deleteModel(modelName); 24 | } 25 | return mongooseClient.model(modelName, schema); 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /generators/authentication/templates/ts/test.mocha.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import app from '../<%= libDirectory %>/app'; 3 | 4 | describe('authentication', () => { 5 | it('registered the authentication service', () => { 6 | assert.ok(app.service('authentication')); 7 | }); 8 | <% if(strategies.includes('local')) { %> 9 | describe('local strategy', () => { 10 | const userInfo = { 11 | email: 'someone@example.com', 12 | password: 'supersecret' 13 | }; 14 | 15 | before(async () => { 16 | try { 17 | await app.service('users').create(userInfo); 18 | } catch (error) { 19 | // Do nothing, it just means the user already exists and can be tested 20 | } 21 | }); 22 | 23 | it('authenticates user and creates accessToken', async () => { 24 | const { user, accessToken } = await app.service('authentication').create({ 25 | strategy: 'local', 26 | ...userInfo 27 | }, {}); 28 | 29 | assert.ok(accessToken, 'Created access token for user'); 30 | assert.ok(user, 'Includes user in authentication data'); 31 | }); 32 | });<% } %> 33 | }); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Feathers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /generators/authentication/templates/js/test.mocha.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const app = require('../<%= libDirectory %>/app'); 3 | 4 | describe('authentication', () => { 5 | it('registered the authentication service', () => { 6 | assert.ok(app.service('authentication')); 7 | }); 8 | <% if(strategies.includes('local')) { %> 9 | describe('local strategy', () => { 10 | const userInfo = { 11 | email: 'someone@example.com', 12 | password: 'supersecret' 13 | }; 14 | 15 | before(async () => { 16 | try { 17 | await app.service('users').create(userInfo); 18 | } catch (error) { 19 | // Do nothing, it just means the user already exists and can be tested 20 | } 21 | }); 22 | 23 | it('authenticates user and creates accessToken', async () => { 24 | const { user, accessToken } = await app.service('authentication').create({ 25 | strategy: 'local', 26 | ...userInfo 27 | }); 28 | 29 | assert.ok(accessToken, 'Created access token for user'); 30 | assert.ok(user, 'Includes user in authentication data'); 31 | }); 32 | });<% } %> 33 | }); 34 | -------------------------------------------------------------------------------- /generators/service/templates/js/model/sequelize-user.js: -------------------------------------------------------------------------------- 1 | // See https://sequelize.org/master/manual/model-basics.html 2 | // for more of what you can do here. 3 | const Sequelize = require('sequelize'); 4 | const DataTypes = Sequelize.DataTypes; 5 | 6 | module.exports = function (app) { 7 | const sequelizeClient = app.get('sequelizeClient'); 8 | const <%= camelName %> = sequelizeClient.define('<%= snakeName %>', { 9 | <% if(authentication.strategies.indexOf('local') !== -1) { %> 10 | email: { 11 | type: DataTypes.STRING, 12 | allowNull: false, 13 | unique: true 14 | }, 15 | password: { 16 | type: DataTypes.STRING, 17 | allowNull: false 18 | }, 19 | <% } %> 20 | <% authentication.oauthProviders.forEach(provider => { %> 21 | <%= provider %>Id: { type: DataTypes.STRING }, 22 | <% }); %> 23 | }, { 24 | hooks: { 25 | beforeCount(options) { 26 | options.raw = true; 27 | } 28 | } 29 | }); 30 | 31 | // eslint-disable-next-line no-unused-vars 32 | <%= camelName %>.associate = function (models) { 33 | // Define associations here 34 | // See https://sequelize.org/master/manual/assocs.html 35 | }; 36 | 37 | return <%= camelName %>; 38 | }; 39 | -------------------------------------------------------------------------------- /generators/service/templates/ts/model/mongoose-user.ts: -------------------------------------------------------------------------------- 1 | // <%= name %>-model.ts - A mongoose model 2 | // 3 | // See http://mongoosejs.com/docs/models.html 4 | // for more of what you can do here. 5 | import { Application } from '../declarations'; 6 | import { Model, Mongoose } from 'mongoose'; 7 | 8 | export default function (app: Application): Model { 9 | const modelName = '<%= camelName %>'; 10 | const mongooseClient: Mongoose = app.get('mongooseClient'); 11 | const schema = new mongooseClient.Schema({ 12 | <% if(authentication.strategies.indexOf('local') !== -1) { %> 13 | email: { type: String, unique: true, lowercase: true }, 14 | password: { type: String }, 15 | <% } %> 16 | <% authentication.oauthProviders.forEach(provider => { %> 17 | <%= provider %>Id: { type: String }, 18 | <% }); %> 19 | }, { 20 | timestamps: true 21 | }); 22 | 23 | // This is necessary to avoid model compilation errors in watch mode 24 | // see https://mongoosejs.com/docs/api/connection.html#connection_Connection-deleteModel 25 | if (mongooseClient.modelNames().includes(modelName)) { 26 | (mongooseClient as any).deleteModel(modelName); 27 | } 28 | return mongooseClient.model(modelName, schema); 29 | } 30 | -------------------------------------------------------------------------------- /generators/app/templates/js/README.md: -------------------------------------------------------------------------------- 1 | # <%= name %> 2 | 3 | > <%= description %> 4 | 5 | ## About 6 | 7 | This project uses [Feathers](http://feathersjs.com). An open source web framework for building modern real-time applications. 8 | 9 | ## Getting Started 10 | 11 | Getting up and running is as easy as 1, 2, 3. 12 | 13 | 1. Make sure you have [NodeJS](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed. 14 | 2. Install your dependencies 15 | 16 | ``` 17 | cd path/to/<%= name %> 18 | npm install 19 | ``` 20 | 21 | 3. Start your app 22 | 23 | ``` 24 | npm start 25 | ``` 26 | 27 | ## Testing 28 | 29 | Simply run `npm test` and all your tests in the `test/` directory will be run. 30 | 31 | ## Scaffolding 32 | 33 | Feathers has a powerful command line interface. Here are a few things it can do: 34 | 35 | ``` 36 | $ npm install -g @feathersjs/cli # Install Feathers CLI 37 | 38 | $ feathers generate service # Generate a new Service 39 | $ feathers generate hook # Generate a new Hook 40 | $ feathers help # Show all commands 41 | ``` 42 | 43 | ## Help 44 | 45 | For more information on all the things you can do with Feathers visit [docs.feathersjs.com](http://docs.feathersjs.com). 46 | -------------------------------------------------------------------------------- /generators/app/templates/ts/README.md: -------------------------------------------------------------------------------- 1 | # <%= name %> 2 | 3 | > <%= description %> 4 | 5 | ## About 6 | 7 | This project uses [Feathers](http://feathersjs.com). An open source web framework for building modern real-time applications. 8 | 9 | ## Getting Started 10 | 11 | Getting up and running is as easy as 1, 2, 3. 12 | 13 | 1. Make sure you have [NodeJS](https://nodejs.org/) and [npm](https://www.npmjs.com/) installed. 14 | 2. Install your dependencies 15 | 16 | ``` 17 | cd path/to/<%= name %> 18 | npm install 19 | ``` 20 | 21 | 3. Start your app 22 | 23 | ``` 24 | npm start 25 | ``` 26 | 27 | ## Testing 28 | 29 | Simply run `npm test` and all your tests in the `test/` directory will be run. 30 | 31 | ## Scaffolding 32 | 33 | Feathers has a powerful command line interface. Here are a few things it can do: 34 | 35 | ``` 36 | $ npm install -g @feathersjs/cli # Install Feathers CLI 37 | 38 | $ feathers generate service # Generate a new Service 39 | $ feathers generate hook # Generate a new Hook 40 | $ feathers help # Show all commands 41 | ``` 42 | 43 | ## Help 44 | 45 | For more information on all the things you can do with Feathers visit [docs.feathersjs.com](http://docs.feathersjs.com). 46 | -------------------------------------------------------------------------------- /generators/service/templates/ts/service.ts: -------------------------------------------------------------------------------- 1 | // Initializes the `<%= name %>` service on path `/<%= path %>` 2 | import { ServiceAddons } from '@feathersjs/feathers'; 3 | import { Application } from '<%= relativeRoot %>declarations'; 4 | import { <%= className %> } from './<%= kebabName %>.class';<% if(modelName) { %> 5 | import createModel from '<%= relativeRoot %>models/<%= modelName %>';<% } %> 6 | import hooks from './<%= kebabName %>.hooks'; 7 | 8 | // Add this service to the service type index 9 | declare module '<%= relativeRoot %>declarations' { 10 | interface ServiceTypes { 11 | '<%= path %>': <%= className %> & ServiceAddons; 12 | } 13 | } 14 | 15 | export default function (app: Application): void { 16 | const options = {<% if (modelName) { %> 17 | Model: createModel(app),<% } %><% if (serviceModule === 'feathers-prisma') {%> 18 | model: '<%= className[0].toLowerCase() + className.substr(1) %>', 19 | client: app.get('prisma'),<% } %> 20 | paginate: app.get('paginate') 21 | }; 22 | 23 | // Initialize our service with any options it requires 24 | app.use('/<%= path %>', new <%= className %>(options, app)); 25 | 26 | // Get our initialized service so that we can register hooks 27 | const service = app.service('<%= path %>'); 28 | 29 | service.hooks(hooks); 30 | } 31 | -------------------------------------------------------------------------------- /generators/service/templates/js/model/cassandra-user.js: -------------------------------------------------------------------------------- 1 | // See https://express-cassandra.readthedocs.io/en/latest/schema/ 2 | // for more of what you can do here. 3 | module.exports = function (app) { 4 | const models = app.get('models'); 5 | const <%= camelName %> = models.loadSchema('<%= camelName %>', { 6 | table_name: '<%= snakeName %>', 7 | fields: { 8 | id: 'int', 9 | <% if(authentication.strategies.includes('local')) { %> 10 | email: 'text', 11 | password: { 12 | type: 'text', 13 | rule: { 14 | required: true 15 | } 16 | }, 17 | <% } %><% authentication.oauthProviders.forEach(provider => { %> 18 | <%= provider %>Id: 'text', 19 | <% }); %> 20 | }, 21 | key: ['id'], 22 | custom_indexes: [ 23 | { 24 | on: 'email', 25 | using: 'org.apache.cassandra.index.sasi.SASIIndex', 26 | options: {} 27 | }, 28 | { 29 | on: 'password', 30 | using: 'org.apache.cassandra.index.sasi.SASIIndex', 31 | options: {} 32 | } 33 | ], 34 | options: { 35 | timestamps: true 36 | } 37 | }, function (err) { 38 | if (err) throw err; 39 | }); 40 | 41 | <%= camelName %>.syncDB(function (err) { 42 | if (err) throw err; 43 | }); 44 | 45 | return <%= camelName %>; 46 | }; 47 | -------------------------------------------------------------------------------- /generators/app/configs/eslintrc.json.js: -------------------------------------------------------------------------------- 1 | module.exports = generator => { 2 | const { props } = generator; 3 | const isTypescript = props.language === 'ts'; 4 | 5 | const config = { 6 | env: { 7 | es6: true, 8 | node: true 9 | }, 10 | parserOptions: { 11 | ecmaVersion: 2018 12 | }, 13 | plugins: null, 14 | extends: ['eslint:recommended'], 15 | rules: { 16 | 'indent': [ 17 | 'error', 18 | 2 19 | ], 20 | 'linebreak-style': [ 21 | 'error', 22 | 'unix' 23 | ], 24 | 'quotes': [ 25 | 'error', 26 | 'single' 27 | ], 28 | 'semi': [ 29 | 'error', 30 | 'always' 31 | ] 32 | } 33 | }; 34 | config.env[props.tester] = true; 35 | 36 | if (isTypescript) { 37 | config.parserOptions = { 38 | parser: '@typescript-eslint/parser', 39 | ecmaVersion: 2018, 40 | sourceType: 'module' 41 | }; 42 | 43 | config.plugins = ['@typescript-eslint']; 44 | config.extends = [ 45 | 'plugin:@typescript-eslint/recommended' 46 | ]; 47 | 48 | // rules 49 | config.rules['@typescript-eslint/no-explicit-any'] = 'off'; 50 | config.rules['@typescript-eslint/no-empty-interface'] = 'off'; 51 | } else { 52 | delete config.plugins; 53 | } 54 | 55 | return config; 56 | }; 57 | -------------------------------------------------------------------------------- /generators/service/templates/ts/model/sequelize-user.ts: -------------------------------------------------------------------------------- 1 | // See https://sequelize.org/master/manual/model-basics.html 2 | // for more of what you can do here. 3 | import { Sequelize, DataTypes, Model } from 'sequelize'; 4 | import { Application } from '../declarations'; 5 | import { HookReturn } from 'sequelize/types/hooks'; 6 | 7 | export default function (app: Application): typeof Model { 8 | const sequelizeClient: Sequelize = app.get('sequelizeClient'); 9 | const <%= camelName %> = sequelizeClient.define('<%= snakeName %>', { 10 | <% if(authentication.strategies.indexOf('local') !== -1) { %> 11 | email: { 12 | type: DataTypes.STRING, 13 | allowNull: false, 14 | unique: true 15 | }, 16 | password: { 17 | type: DataTypes.STRING, 18 | allowNull: false 19 | }, 20 | <% } %> 21 | <% authentication.oauthProviders.forEach(provider => { %> 22 | <%= provider %>Id: { type: DataTypes.STRING }, 23 | <% }); %> 24 | }, { 25 | hooks: { 26 | beforeCount(options: any): HookReturn { 27 | options.raw = true; 28 | } 29 | } 30 | }); 31 | 32 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 33 | (<%= camelName %> as any).associate = function (models: any): void { 34 | // Define associations here 35 | // See https://sequelize.org/master/manual/assocs.html 36 | }; 37 | 38 | return <%= camelName %>; 39 | } 40 | -------------------------------------------------------------------------------- /generators/service/templates/js/hooks-user.js: -------------------------------------------------------------------------------- 1 | const { authenticate } = require('@feathersjs/authentication').hooks; 2 | 3 | <% if (authentication.strategies.indexOf('local') !== -1) { %>const { 4 | hashPassword, protect 5 | } = require('@feathersjs/authentication-local').hooks;<% } %> 6 | 7 | module.exports = { 8 | before: { 9 | all: [], 10 | find: [ authenticate('jwt') ], 11 | get: [ authenticate('jwt') ], 12 | create: [ <% if (authentication.strategies.indexOf('local') !== -1) { %>hashPassword('password')<% } %> ], 13 | update: [ <% if (authentication.strategies.indexOf('local') !== -1) { %>hashPassword('password'), <% } %> authenticate('jwt') ], 14 | patch: [ <% if (authentication.strategies.indexOf('local') !== -1) { %>hashPassword('password'), <% } %> authenticate('jwt') ], 15 | remove: [ authenticate('jwt') ] 16 | }, 17 | 18 | after: { 19 | all: [ <% if (authentication.strategies.indexOf('local') !== -1) { %> 20 | // Make sure the password field is never sent to the client 21 | // Always must be the last hook 22 | protect('password')<% } %> 23 | ], 24 | find: [], 25 | get: [], 26 | create: [], 27 | update: [], 28 | patch: [], 29 | remove: [] 30 | }, 31 | 32 | error: { 33 | all: [], 34 | find: [], 35 | get: [], 36 | create: [], 37 | update: [], 38 | patch: [], 39 | remove: [] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /generators/service/templates/ts/model/cassandra-user.ts: -------------------------------------------------------------------------------- 1 | // See https://express-cassandra.readthedocs.io/en/latest/schema/ 2 | // for more of what you can do here. 3 | import { Application } from '../declarations'; 4 | 5 | export default function (app: Application): any { 6 | const models = app.get('models'); 7 | const <%= camelName %> = models.loadSchema('<%= camelName %>', { 8 | table_name: '<%= snakeName %>', 9 | fields: { 10 | id: 'int', 11 | <% if(authentication.strategies.indexOf('local') !== -1) { %> 12 | email: 'text', 13 | password: { 14 | type: 'text', 15 | rule: { 16 | required: true 17 | } 18 | }, 19 | <% } %><% authentication.oauthProviders.forEach(provider => { %> 20 | <%= provider %>Id: 'text', 21 | <% }); %> 22 | }, 23 | key: ['id'], 24 | custom_indexes: [ 25 | { 26 | on: 'email', 27 | using: 'org.apache.cassandra.index.sasi.SASIIndex', 28 | options: {} 29 | }, 30 | { 31 | on: 'password', 32 | using: 'org.apache.cassandra.index.sasi.SASIIndex', 33 | options: {} 34 | } 35 | ], 36 | options: { 37 | timestamps: true 38 | } 39 | }, function (err: any) { 40 | if (err) throw err; 41 | }); 42 | 43 | <%= camelName %>.syncDB(function (err: any) { 44 | if (err) throw err; 45 | }); 46 | 47 | return <%= camelName %>; 48 | } 49 | -------------------------------------------------------------------------------- /generators/service/templates/js/model/objection.js: -------------------------------------------------------------------------------- 1 | // See https://vincit.github.io/objection.js/#models 2 | // for more of what you can do here. 3 | const { Model } = require('objection'); 4 | 5 | class <%= className %> extends Model { 6 | 7 | static get tableName() { 8 | return '<%= snakeName %>'; 9 | } 10 | 11 | static get jsonSchema() { 12 | return { 13 | type: 'object', 14 | required: ['text'], 15 | 16 | properties: { 17 | text: { type: 'string' } 18 | } 19 | }; 20 | } 21 | 22 | $beforeInsert() { 23 | this.createdAt = this.updatedAt = new Date().toISOString(); 24 | } 25 | 26 | $beforeUpdate() { 27 | this.updatedAt = new Date().toISOString(); 28 | } 29 | } 30 | 31 | module.exports = function (app) { 32 | const db = app.get('knex'); 33 | 34 | db.schema.hasTable('<%= snakeName %>').then(exists => { 35 | if (!exists) { 36 | db.schema.createTable('<%= snakeName %>', table => { 37 | table.increments('id'); 38 | table.string('text'); 39 | table.timestamp('createdAt'); 40 | table.timestamp('updatedAt'); 41 | }) 42 | .then(() => console.log('Created <%= snakeName %> table')) // eslint-disable-line no-console 43 | .catch(e => console.error('Error creating <%= snakeName %> table', e)); // eslint-disable-line no-console 44 | } 45 | }) 46 | .catch(e => console.error('Error creating <%= snakeName %> table', e)); // eslint-disable-line no-console 47 | 48 | return <%= className %>; 49 | }; 50 | -------------------------------------------------------------------------------- /generators/service/templates/ts/hooks-user.ts: -------------------------------------------------------------------------------- 1 | import * as feathersAuthentication from '@feathersjs/authentication';<% if (authentication.strategies.indexOf('local') !== -1) { %> 2 | import * as local from '@feathersjs/authentication-local';<% } %> 3 | // Don't remove this comment. It's needed to format import lines nicely. 4 | 5 | const { authenticate } = feathersAuthentication.hooks;<% if (authentication.strategies.indexOf('local') !== -1) { %> 6 | const { hashPassword, protect } = local.hooks;<% } %> 7 | 8 | export default { 9 | before: { 10 | all: [], 11 | find: [ authenticate('jwt') ], 12 | get: [ authenticate('jwt') ], 13 | create: [ <% if (authentication.strategies.indexOf('local') !== -1) { %>hashPassword('password')<% } %> ], 14 | update: [ <% if (authentication.strategies.indexOf('local') !== -1) { %>hashPassword('password'), <% } %> authenticate('jwt') ], 15 | patch: [ <% if (authentication.strategies.indexOf('local') !== -1) { %>hashPassword('password'), <% } %> authenticate('jwt') ], 16 | remove: [ authenticate('jwt') ] 17 | }, 18 | 19 | after: { 20 | all: [ <% if (authentication.strategies.indexOf('local') !== -1) { %> 21 | // Make sure the password field is never sent to the client 22 | // Always must be the last hook 23 | protect('password')<% } %> 24 | ], 25 | find: [], 26 | get: [], 27 | create: [], 28 | update: [], 29 | patch: [], 30 | remove: [] 31 | }, 32 | 33 | error: { 34 | all: [], 35 | find: [], 36 | get: [], 37 | create: [], 38 | update: [], 39 | patch: [], 40 | remove: [] 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /generators/service/templates/ts/model/objection.ts: -------------------------------------------------------------------------------- 1 | // See https://vincit.github.io/objection.js/#models 2 | // for more of what you can do here. 3 | import { Model, JSONSchema } from 'objection'; 4 | import { Knex } from 'knex'; 5 | import { Application } from '../declarations'; 6 | 7 | class <%= className %> extends Model { 8 | createdAt!: string; 9 | updatedAt!: string; 10 | 11 | static get tableName(): string { 12 | return '<%= snakeName %>'; 13 | } 14 | 15 | static get jsonSchema(): JSONSchema { 16 | return { 17 | type: 'object', 18 | required: ['text'], 19 | 20 | properties: { 21 | text: { type: 'string' } 22 | } 23 | }; 24 | } 25 | 26 | $beforeInsert(): void { 27 | this.createdAt = this.updatedAt = new Date().toISOString(); 28 | } 29 | 30 | $beforeUpdate(): void { 31 | this.updatedAt = new Date().toISOString(); 32 | } 33 | } 34 | 35 | export default function (app: Application): typeof <%= className %> { 36 | const db: Knex = app.get('knex'); 37 | 38 | db.schema.hasTable('<%= snakeName %>').then(exists => { 39 | if (!exists) { 40 | db.schema.createTable('<%= snakeName %>', table => { 41 | table.increments('id'); 42 | table.string('text'); 43 | table.timestamp('createdAt'); 44 | table.timestamp('updatedAt'); 45 | }) 46 | .then(() => console.log('Created <%= snakeName %> table')) // eslint-disable-line no-console 47 | .catch(e => console.error('Error creating <%= snakeName %> table', e)); // eslint-disable-line no-console 48 | } 49 | }) 50 | .catch(e => console.error('Error creating <%= snakeName %> table', e)); // eslint-disable-line no-console 51 | 52 | return <%= className %>; 53 | } 54 | -------------------------------------------------------------------------------- /generators/service/templates/ts/types/generic.ts: -------------------------------------------------------------------------------- 1 | import { Id, NullableId, Paginated, Params, ServiceMethods } from '@feathersjs/feathers'; 2 | import { Application } from '<%= relativeRoot %>declarations'; 3 | 4 | interface Data {} 5 | 6 | interface ServiceOptions {} 7 | 8 | export class <%= className %> implements ServiceMethods { 9 | app: Application; 10 | options: ServiceOptions; 11 | 12 | constructor (options: ServiceOptions = {}, app: Application) { 13 | this.options = options; 14 | this.app = app; 15 | } 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | async find (params?: Params): Promise> { 19 | return []; 20 | } 21 | 22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 | async get (id: Id, params?: Params): Promise { 24 | return { 25 | id, text: `A new message with ID: ${id}!` 26 | }; 27 | } 28 | 29 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 30 | async create (data: Data, params?: Params): Promise { 31 | if (Array.isArray(data)) { 32 | return Promise.all(data.map(current => this.create(current, params))); 33 | } 34 | 35 | return data; 36 | } 37 | 38 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 39 | async update (id: NullableId, data: Data, params?: Params): Promise { 40 | return data; 41 | } 42 | 43 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 44 | async patch (id: NullableId, data: Data, params?: Params): Promise { 45 | return data; 46 | } 47 | 48 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 49 | async remove (id: NullableId, params?: Params): Promise { 50 | return { id }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /generators/app/templates/js/app.test.jest.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const url = require('url'); 3 | const app = require('../<%= src %>/app'); 4 | 5 | const port = app.get('port') || 8998; 6 | const getUrl = pathname => url.format({ 7 | hostname: app.get('host') || 'localhost', 8 | protocol: 'http', 9 | port, 10 | pathname 11 | }); 12 | 13 | describe('Feathers application tests (with jest)', () => { 14 | let server; 15 | 16 | beforeAll(done => { 17 | server = app.listen(port); 18 | server.once('listening', () => done()); 19 | }); 20 | 21 | afterAll(done => { 22 | server.close(done); 23 | }); 24 | 25 | it('starts and shows the index page', async () => { 26 | expect.assertions(1); 27 | 28 | const { data } = await axios.get(getUrl()); 29 | 30 | expect(data.indexOf('')).not.toBe(-1); 31 | }); 32 | 33 | describe('404', () => { 34 | it('shows a 404 HTML page', async () => { 35 | expect.assertions(2); 36 | try { 37 | await axios.get(getUrl('path/to/nowhere'), { 38 | headers: { 39 | 'Accept': 'text/html' 40 | } 41 | }); 42 | } catch (error) { 43 | const { response } = error; 44 | 45 | expect(response.status).toBe(404); 46 | expect(response.data.indexOf('')).not.toBe(-1); 47 | } 48 | }); 49 | 50 | it('shows a 404 JSON error without stack trace', async () => { 51 | expect.assertions(4); 52 | 53 | try { 54 | await axios.get(getUrl('path/to/nowhere')); 55 | } catch (error) { 56 | const { response } = error; 57 | 58 | expect(response.status).toBe(404); 59 | expect(response.data.code).toBe(404); 60 | expect(response.data.message).toBe('Page not found'); 61 | expect(response.data.name).toBe('NotFound'); 62 | } 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /generators/app/templates/js/app.test.mocha.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert').strict; 2 | const axios = require('axios'); 3 | const url = require('url'); 4 | const app = require('../<%= src %>/app'); 5 | 6 | const port = app.get('port') || 8998; 7 | const getUrl = pathname => url.format({ 8 | hostname: app.get('host') || 'localhost', 9 | protocol: 'http', 10 | port, 11 | pathname 12 | }); 13 | 14 | describe('Feathers application tests', () => { 15 | let server; 16 | 17 | before(function(done) { 18 | server = app.listen(port); 19 | server.once('listening', () => done()); 20 | }); 21 | 22 | after(function(done) { 23 | server.close(done); 24 | }); 25 | 26 | it('starts and shows the index page', async () => { 27 | const { data } = await axios.get(getUrl()); 28 | 29 | assert.ok(data.indexOf('') !== -1); 30 | }); 31 | 32 | describe('404', function() { 33 | it('shows a 404 HTML page', async () => { 34 | try { 35 | await axios.get(getUrl('path/to/nowhere'), { 36 | headers: { 37 | 'Accept': 'text/html' 38 | } 39 | }); 40 | assert.fail('should never get here'); 41 | } catch (error) { 42 | const { response } = error; 43 | 44 | assert.equal(response.status, 404); 45 | assert.ok(response.data.indexOf('') !== -1); 46 | } 47 | }); 48 | 49 | it('shows a 404 JSON error without stack trace', async () => { 50 | try { 51 | await axios.get(getUrl('path/to/nowhere'), { 52 | json: true 53 | }); 54 | assert.fail('should never get here'); 55 | } catch (error) { 56 | const { response } = error; 57 | 58 | assert.equal(response.status, 404); 59 | assert.equal(response.data.code, 404); 60 | assert.equal(response.data.message, 'Page not found'); 61 | assert.equal(response.data.name, 'NotFound'); 62 | } 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /generators/app/templates/ts/app.test.mocha.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { Server } from 'http'; 3 | import url from 'url'; 4 | import axios from 'axios'; 5 | 6 | import app from '../<%= src %>/app'; 7 | 8 | const port = app.get('port') || 8998; 9 | const getUrl = (pathname?: string): string => url.format({ 10 | hostname: app.get('host') || 'localhost', 11 | protocol: 'http', 12 | port, 13 | pathname 14 | }); 15 | 16 | describe('Feathers application tests', () => { 17 | let server: Server; 18 | 19 | before(function(done) { 20 | server = app.listen(port); 21 | server.once('listening', () => done()); 22 | }); 23 | 24 | after(function(done) { 25 | server.close(done); 26 | }); 27 | 28 | it('starts and shows the index page', async () => { 29 | const { data } = await axios.get(getUrl()); 30 | 31 | assert.ok(data.indexOf('') !== -1); 32 | }); 33 | 34 | describe('404', function() { 35 | it('shows a 404 HTML page', async () => { 36 | try { 37 | await axios.get(getUrl('path/to/nowhere'), { 38 | headers: { 39 | 'Accept': 'text/html' 40 | } 41 | }); 42 | assert.fail('should never get here'); 43 | } catch (error: any) { 44 | const { response } = error; 45 | 46 | assert.equal(response.status, 404); 47 | assert.ok(response.data.indexOf('') !== -1); 48 | } 49 | }); 50 | 51 | it('shows a 404 JSON error without stack trace', async () => { 52 | try { 53 | await axios.get(getUrl('path/to/nowhere')); 54 | assert.fail('should never get here'); 55 | } catch (error: any) { 56 | const { response } = error; 57 | 58 | assert.equal(response.status, 404); 59 | assert.equal(response.data.code, 404); 60 | assert.equal(response.data.message, 'Page not found'); 61 | assert.equal(response.data.name, 'NotFound'); 62 | } 63 | }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /generators/app/templates/ts/app.test.jest.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | import { Server } from 'http'; 3 | import url from 'url'; 4 | import axios from 'axios'; 5 | 6 | import app from '../<%= src %>/app'; 7 | 8 | const port = app.get('port') || 8998; 9 | const getUrl = (pathname?: string): string => url.format({ 10 | hostname: app.get('host') || 'localhost', 11 | protocol: 'http', 12 | port, 13 | pathname 14 | }); 15 | 16 | describe('Feathers application tests (with jest)', () => { 17 | let server: Server; 18 | 19 | beforeAll(done => { 20 | server = app.listen(port); 21 | server.once('listening', () => done()); 22 | }); 23 | 24 | afterAll(done => { 25 | server.close(done); 26 | }); 27 | 28 | it('starts and shows the index page', async () => { 29 | expect.assertions(1); 30 | 31 | const { data } = await axios.get(getUrl()); 32 | 33 | expect(data.indexOf('')).not.toBe(-1); 34 | }); 35 | 36 | describe('404', () => { 37 | it('shows a 404 HTML page', async () => { 38 | expect.assertions(2); 39 | 40 | try { 41 | await axios.get(getUrl('path/to/nowhere'), { 42 | headers: { 43 | 'Accept': 'text/html' 44 | } 45 | }); 46 | } catch (error: any) { 47 | const { response } = error; 48 | 49 | expect(response.status).toBe(404); 50 | expect(response.data.indexOf('')).not.toBe(-1); 51 | } 52 | }); 53 | 54 | it('shows a 404 JSON error without stack trace', async () => { 55 | expect.assertions(4); 56 | 57 | try { 58 | await axios.get(getUrl('path/to/nowhere')); 59 | } catch (error: any) { 60 | const { response } = error; 61 | 62 | expect(response.status).toBe(404); 63 | expect(response.data.code).toBe(404); 64 | expect(response.data.message).toBe('Page not found'); 65 | expect(response.data.name).toBe('NotFound'); 66 | } 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /generators/service/templates/js/model/objection-user.js: -------------------------------------------------------------------------------- 1 | // See https://vincit.github.io/objection.js/#models 2 | // for more of what you can do here. 3 | const { Model } = require('objection'); 4 | 5 | class <%= className %> extends Model { 6 | 7 | static get tableName() { 8 | return '<%= snakeName %>'; 9 | } 10 | 11 | static get jsonSchema() { 12 | return { 13 | type: 'object', 14 | required: ['password'], 15 | 16 | properties: { 17 | <% if(authentication.strategies.indexOf('local') !== -1) { %> 18 | email: { type: ['string', 'null'] }, 19 | password: 'string', 20 | <% } %><% authentication.oauthProviders.forEach(provider => { %> 21 | <%= provider %>Id: { type: 'string' }, 22 | <% }); %> 23 | } 24 | }; 25 | } 26 | 27 | $beforeInsert() { 28 | this.createdAt = this.updatedAt = new Date().toISOString(); 29 | } 30 | 31 | $beforeUpdate() { 32 | this.updatedAt = new Date().toISOString(); 33 | } 34 | } 35 | 36 | module.exports = function (app) { 37 | const db = app.get('knex'); 38 | 39 | db.schema.hasTable('<%= snakeName %>').then(exists => { 40 | if (!exists) { 41 | db.schema.createTable('<%= snakeName %>', table => { 42 | table.increments('id'); 43 | <% if(authentication.strategies.indexOf('local') !== -1) { %> 44 | table.string('email').unique(); 45 | table.string('password'); 46 | <% } %> 47 | <% authentication.oauthProviders.forEach(provider => { %> 48 | table.string('<%= provider %>Id'); 49 | <% }); %> 50 | table.timestamp('createdAt'); 51 | table.timestamp('updatedAt'); 52 | }) 53 | .then(() => console.log('Created <%= snakeName %> table')) // eslint-disable-line no-console 54 | .catch(e => console.error('Error creating <%= snakeName %> table', e)); // eslint-disable-line no-console 55 | } 56 | }) 57 | .catch(e => console.error('Error creating <%= snakeName %> table', e)); // eslint-disable-line no-console 58 | 59 | return <%= className %>; 60 | }; 61 | -------------------------------------------------------------------------------- /generators/app/templates/js/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const favicon = require('serve-favicon'); 3 | const compress = require('compression'); 4 | const helmet = require('helmet'); 5 | const cors = require('cors'); 6 | const logger = require('./logger'); 7 | 8 | const feathers = require('@feathersjs/feathers'); 9 | const configuration = require('@feathersjs/configuration'); 10 | const express = require('@feathersjs/express'); 11 | <% if (hasProvider('socketio')) { %>const socketio = require('@feathersjs/socketio');<% } %> 12 | <% if (hasProvider('primus')) { %>const primus = require('@feathersjs/primus');<% } %> 13 | 14 | const middleware = require('./middleware'); 15 | const services = require('./services'); 16 | const appHooks = require('./app.hooks'); 17 | const channels = require('./channels'); 18 | 19 | const app = express(feathers()); 20 | 21 | // Load app configuration 22 | app.configure(configuration()); 23 | // Enable security, CORS, compression, favicon and body parsing 24 | app.use(helmet({ 25 | contentSecurityPolicy: false 26 | })); 27 | app.use(cors()); 28 | app.use(compress()); 29 | app.use(express.json()); 30 | app.use(express.urlencoded({ extended: true })); 31 | app.use(favicon(path.join(app.get('public'), 'favicon.ico'))); 32 | // Host the public folder 33 | app.use('/', express.static(app.get('public'))); 34 | 35 | // Set up Plugins and providers 36 | <% if (hasProvider('rest')) { %>app.configure(express.rest());<% } %> 37 | <% if (hasProvider('socketio')) { %>app.configure(socketio());<% } %> 38 | <% if(hasProvider('primus')) { %>app.configure(primus({ transformer: 'websockets' }));<% } %> 39 | // Configure other middleware (see `middleware/index.js`) 40 | app.configure(middleware); 41 | // Set up our services (see `services/index.js`) 42 | app.configure(services); 43 | // Set up event channels (see channels.js) 44 | app.configure(channels); 45 | 46 | // Configure a middleware for 404s and the error handler 47 | app.use(express.notFound()); 48 | app.use(express.errorHandler({ logger })); 49 | 50 | app.hooks(appHooks); 51 | 52 | module.exports = app; 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # generator-feathers 2 | 3 | > __Important:__ The Feathers generator has been moved to the [cli package](https://github.com/feathersjs/feathers/tree/dove/packages/cli) in [feathersjs/feathers](https://github.com/feathersjs/feathers ) 4 | 5 | [![CI](https://github.com/feathersjs/generator-feathers/workflows/CI/badge.svg)](https://github.com/feathersjs/generator-feathers/actions?query=workflow%3ACI) 6 | 7 | > A Yeoman generator for a Feathers application 8 | 9 | ## Installation 10 | 11 | First you need install [yeoman](http://yeoman.io/). 12 | 13 | ```bash 14 | npm install -g yo 15 | ``` 16 | 17 | Then install the feathers generator. 18 | 19 | ```bash 20 | npm install -g yo generator-feathers 21 | ``` 22 | 23 | ## Usage 24 | 25 | Create a directory for your new app. 26 | 27 | ```bash 28 | mkdir my-new-app; cd my-new-app/ 29 | ``` 30 | 31 | Generate your app and follow the prompts. 32 | 33 | ```bash 34 | yo feathers 35 | ``` 36 | 37 | Start your brand new app! 💥 38 | 39 | ```bash 40 | npm start 41 | ``` 42 | 43 | ## Available commands 44 | 45 | ```bash 46 | # short alias for generate new application 47 | yo feathers 48 | 49 | # set up authentication 50 | yo feathers:authentication 51 | 52 | # set up a database connection 53 | yo feathers:connection 54 | 55 | # generate new hook 56 | yo feathers:hook 57 | 58 | # generate new middleware 59 | yo feathers:middleware 60 | 61 | # generate new service 62 | yo feathers:service 63 | ``` 64 | 65 | ## Production 66 | [feathers/feathers-configuration](https://github.com/feathersjs/feathers-configuration) uses `NODE_ENV` to find a configuration file under `config/`. After updating `config/production.js` you can run 67 | 68 | ```bash 69 | NODE_ENV=production npm start 70 | ``` 71 | 72 | ## Contributing 73 | 74 | To contribute PRs for these generators, you will need to clone the repo 75 | then inside the repo's directory, run `npm link`. This sets up a global 76 | link to your local package for running tests (`npm test`) and generating 77 | new feathers apps/services/hooks/etc. 78 | 79 | When finished testing, optionally run `npm uninstall generator-feathers` to remove 80 | the link. 81 | 82 | ## License 83 | 84 | Copyright (c) 2017 85 | 86 | Licensed under the [MIT license](LICENSE). 87 | -------------------------------------------------------------------------------- /generators/service/templates/ts/model/objection-user.ts: -------------------------------------------------------------------------------- 1 | // See https://vincit.github.io/objection.js/#models 2 | // for more of what you can do here. 3 | import { Model, JSONSchema } from 'objection'; 4 | import Knex from 'knex'; 5 | import { Application } from '../declarations'; 6 | 7 | class <%= className %> extends Model { 8 | createdAt!: string; 9 | updatedAt!: string; 10 | 11 | static get tableName(): string { 12 | return '<%= snakeName %>'; 13 | } 14 | 15 | static get jsonSchema(): JSONSchema { 16 | return { 17 | type: 'object', 18 | required: ['password'], 19 | 20 | properties: { 21 | <% if(authentication.strategies.indexOf('local') !== -1) { %> 22 | email: { type: ['string', 'null'] }, 23 | password: { type: 'string' }, 24 | <% } %><% authentication.oauthProviders.forEach(provider => { %> 25 | <%= provider %>Id: { type: 'string' }, 26 | <% }); %> 27 | } 28 | }; 29 | } 30 | 31 | $beforeInsert(): void { 32 | this.createdAt = this.updatedAt = new Date().toISOString(); 33 | } 34 | 35 | $beforeUpdate(): void { 36 | this.updatedAt = new Date().toISOString(); 37 | } 38 | } 39 | 40 | export default function (app: Application): typeof <%= className %> { 41 | const db: Knex = app.get('knex'); 42 | 43 | db.schema.hasTable('<%= snakeName %>').then(exists => { 44 | if (!exists) { 45 | db.schema.createTable('<%= snakeName %>', table => { 46 | table.increments('id'); 47 | <% if(authentication.strategies.indexOf('local') !== -1) { %> 48 | table.string('email').unique(); 49 | table.string('password'); 50 | <% } %> 51 | <% authentication.oauthProviders.forEach(provider => { %> 52 | table.string('<%= provider %>Id'); 53 | <% }); %> 54 | table.timestamp('createdAt'); 55 | table.timestamp('updatedAt'); 56 | }) 57 | .then(() => console.log('Created <%= snakeName %> table')) // eslint-disable-line no-console 58 | .catch(e => console.error('Error creating <%= snakeName %> table', e)); // eslint-disable-line no-console 59 | } 60 | }) 61 | .catch(e => console.error('Error creating <%= snakeName %> table', e)); // eslint-disable-line no-console 62 | 63 | return <%= className %>; 64 | } 65 | -------------------------------------------------------------------------------- /generators/app/templates/ts/app.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import favicon from 'serve-favicon'; 3 | import compress from 'compression'; 4 | import helmet from 'helmet'; 5 | import cors from 'cors'; 6 | 7 | import feathers from '@feathersjs/feathers'; 8 | import configuration from '@feathersjs/configuration'; 9 | import express from '@feathersjs/express'; 10 | <% if (hasProvider('socketio')) { %>import socketio from '@feathersjs/socketio';<% } %> 11 | <% if (hasProvider('primus')) { %>import primus from '@feathersjs/primus';<% } %> 12 | 13 | import { Application } from './declarations'; 14 | import logger from './logger'; 15 | import middleware from './middleware'; 16 | import services from './services'; 17 | import appHooks from './app.hooks'; 18 | import channels from './channels'; 19 | import { HookContext as FeathersHookContext } from '@feathersjs/feathers'; 20 | // Don't remove this comment. It's needed to format import lines nicely. 21 | 22 | const app: Application = express(feathers()); 23 | export type HookContext = { app: Application } & FeathersHookContext; 24 | 25 | // Load app configuration 26 | app.configure(configuration()); 27 | // Enable security, CORS, compression, favicon and body parsing 28 | app.use(helmet({ 29 | contentSecurityPolicy: false 30 | })); 31 | app.use(cors()); 32 | app.use(compress()); 33 | app.use(express.json()); 34 | app.use(express.urlencoded({ extended: true })); 35 | app.use(favicon(path.join(app.get('public'), 'favicon.ico'))); 36 | // Host the public folder 37 | app.use('/', express.static(app.get('public'))); 38 | 39 | // Set up Plugins and providers 40 | <% if (hasProvider('rest')) { %>app.configure(express.rest());<% } %> 41 | <% if (hasProvider('socketio')) { %>app.configure(socketio());<% } %> 42 | <% if(hasProvider('primus')) { %>app.configure(primus({ transformer: 'websockets' }));<% } %> 43 | // Configure other middleware (see `middleware/index.ts`) 44 | app.configure(middleware); 45 | // Set up our services (see `services/index.ts`) 46 | app.configure(services); 47 | // Set up event channels (see channels.ts) 48 | app.configure(channels); 49 | 50 | // Configure a middleware for 404s and the error handler 51 | app.use(express.notFound()); 52 | app.use(express.errorHandler({ logger } as any)); 53 | 54 | app.hooks(appHooks); 55 | 56 | export default app; 57 | -------------------------------------------------------------------------------- /generators/app/templates/js/_gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # IDEs and editors (shamelessly copied from @angular/cli's .gitignore) 31 | /.idea 32 | .project 33 | .classpath 34 | .c9/ 35 | *.launch 36 | .settings/ 37 | *.sublime-workspace 38 | 39 | # IDE - VSCode 40 | .vscode/* 41 | !.vscode/tasks.json 42 | !.vscode/launch.json 43 | !.vscode/extensions.json 44 | 45 | ### Linux ### 46 | *~ 47 | 48 | # temporary files which can be created if a process still has a handle open of a deleted file 49 | .fuse_hidden* 50 | 51 | # KDE directory preferences 52 | .directory 53 | 54 | # Linux trash folder which might appear on any partition or disk 55 | .Trash-* 56 | 57 | # .nfs files are created when an open file is removed but is still being accessed 58 | .nfs* 59 | 60 | ### OSX ### 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 | ### Windows ### 89 | # Windows thumbnail cache files 90 | Thumbs.db 91 | ehthumbs.db 92 | ehthumbs_vista.db 93 | 94 | # Folder config file 95 | Desktop.ini 96 | 97 | # Recycle Bin used on file shares 98 | $RECYCLE.BIN/ 99 | 100 | # Windows Installer files 101 | *.cab 102 | *.msi 103 | *.msm 104 | *.msp 105 | 106 | # Windows shortcuts 107 | *.lnk 108 | 109 | # Others 110 | lib/ 111 | data/ 112 | -------------------------------------------------------------------------------- /generators/app/templates/ts/_gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # IDEs and editors (shamelessly copied from @angular/cli's .gitignore) 31 | /.idea 32 | .project 33 | .classpath 34 | .c9/ 35 | *.launch 36 | .settings/ 37 | *.sublime-workspace 38 | 39 | # IDE - VSCode 40 | .vscode/* 41 | !.vscode/settings.json 42 | !.vscode/tasks.json 43 | !.vscode/launch.json 44 | !.vscode/extensions.json 45 | 46 | ### Linux ### 47 | *~ 48 | 49 | # temporary files which can be created if a process still has a handle open of a deleted file 50 | .fuse_hidden* 51 | 52 | # KDE directory preferences 53 | .directory 54 | 55 | # Linux trash folder which might appear on any partition or disk 56 | .Trash-* 57 | 58 | # .nfs files are created when an open file is removed but is still being accessed 59 | .nfs* 60 | 61 | ### OSX ### 62 | *.DS_Store 63 | .AppleDouble 64 | .LSOverride 65 | 66 | # Icon must end with two \r 67 | Icon 68 | 69 | 70 | # Thumbnails 71 | ._* 72 | 73 | # Files that might appear in the root of a volume 74 | .DocumentRevisions-V100 75 | .fseventsd 76 | .Spotlight-V100 77 | .TemporaryItems 78 | .Trashes 79 | .VolumeIcon.icns 80 | .com.apple.timemachine.donotpresent 81 | 82 | # Directories potentially created on remote AFP share 83 | .AppleDB 84 | .AppleDesktop 85 | Network Trash Folder 86 | Temporary Items 87 | .apdisk 88 | 89 | ### Windows ### 90 | # Windows thumbnail cache files 91 | Thumbs.db 92 | ehthumbs.db 93 | ehthumbs_vista.db 94 | 95 | # Folder config file 96 | Desktop.ini 97 | 98 | # Recycle Bin used on file shares 99 | $RECYCLE.BIN/ 100 | 101 | # Windows Installer files 102 | *.cab 103 | *.msi 104 | *.msm 105 | *.msp 106 | 107 | # Windows shortcuts 108 | *.lnk 109 | 110 | # Others 111 | lib/ 112 | data/ 113 | -------------------------------------------------------------------------------- /generators/app/configs/package.json.js: -------------------------------------------------------------------------------- 1 | const p = require('path'); 2 | const semver = require('semver'); 3 | 4 | module.exports = function(generator) { 5 | const major = semver.major(process.version); 6 | const envConfigDir = process.env['NODE_CONFIG_DIR']; 7 | const configDirectory = envConfigDir ? p.join(envConfigDir) : 'config/'; 8 | const { props } = generator; 9 | const lib = props.src; 10 | const [ packager, version ] = props.packager.split('@'); 11 | const isTypescript = props.language === 'ts'; 12 | const lintScripts = { 13 | eslint: !isTypescript ? 14 | `eslint ${lib}/. test/. --config .eslintrc.json --fix` : 15 | `eslint ${lib}/. test/. --config .eslintrc.json --ext .ts --fix`, 16 | standard: 'standard' 17 | }; 18 | 19 | const pkg = { 20 | name: props.name, 21 | description: props.description, 22 | version: '0.0.0', 23 | homepage: '', 24 | private: true, 25 | main: lib, 26 | keywords: [ 27 | 'feathers' 28 | ], 29 | author: { 30 | name: generator.user.git.name(), 31 | email: generator.user.git.email() 32 | }, 33 | contributors: [], 34 | bugs: {}, 35 | directories: { 36 | lib, 37 | test: 'test/', 38 | config: configDirectory 39 | }, 40 | engines: { 41 | node: `^${major}.0.0`, 42 | [packager]: version 43 | }, 44 | scripts: { 45 | test: `${packager} run lint && ${packager} run ${props.tester}`, 46 | lint: lintScripts[props.linter], 47 | dev: `nodemon ${lib}/`, 48 | start: `node ${lib}/` 49 | }, 50 | standard: { 51 | env: [props.tester], 52 | ignore: [] 53 | } 54 | }; 55 | 56 | if ('mocha' === props.tester) { 57 | pkg.scripts['mocha'] = isTypescript ? 'mocha --require ts-node/register --require source-map-support/register "test/**/*.ts" --recursive --exit' : 'mocha test/ --recursive --exit'; 58 | } else { 59 | pkg.scripts['jest'] = 'jest --forceExit'; 60 | } 61 | 62 | if (isTypescript) { 63 | pkg.scripts = { 64 | ...pkg.scripts, 65 | compile: 'shx rm -rf lib/ && tsc', 66 | dev: `ts-node-dev --no-notify ${lib}/`, 67 | test: (props.linter === 'eslint') ? 68 | `${packager} run lint && ${packager} run compile && ${packager} run ${props.tester}` : 69 | `${packager} run compile && ${packager} run ${props.tester}`, 70 | start: `${packager} run compile && node lib/` 71 | }; 72 | pkg.types = 'lib/'; 73 | 74 | if (props.linter === 'standard') delete pkg.scripts.lint; 75 | } 76 | 77 | return pkg; 78 | }; 79 | -------------------------------------------------------------------------------- /generators/upgrade/index.js: -------------------------------------------------------------------------------- 1 | const Generator = require('../../lib/generator'); 2 | const crypto = require('crypto'); 3 | 4 | const oldPackages = [ 5 | '@feathersjs/authentication-oauth1', 6 | '@feathersjs/authentication-oauth2', 7 | '@feathersjs/authentication-jwt' 8 | ]; 9 | 10 | module.exports = class UpgradeGenerator extends Generator { 11 | writing() { 12 | const dependencies = [ '@feathersjs/authentication-oauth' ]; 13 | 14 | Object.keys(this.pkg.dependencies).forEach(name => { 15 | const isCore = name.startsWith('@feathersjs/') && !oldPackages.includes(name); 16 | const isAdapter = name.startsWith('feathers-') && this.generatorPkg.dependencies[name]; 17 | 18 | if (isCore || isAdapter) { 19 | dependencies.push(name); 20 | delete this.pkg.dependencies[name]; 21 | } 22 | }); 23 | 24 | oldPackages.forEach(name => { 25 | delete this.pkg.dependencies[name]; 26 | }); 27 | 28 | this.conflicter.force = true; 29 | 30 | const authFile = this.destinationPath(this.libDirectory, 'authentication.js'); 31 | 32 | if (this.fs.exists(authFile)) { 33 | this.fs.copy(authFile, 34 | this.destinationPath(this.libDirectory, 'authentication.backup.js') 35 | ); 36 | 37 | this.fs.copy( 38 | this.srcTemplatePath('authentication'), 39 | this.srcDestinationPath(this.libDirectory, 'authentication') 40 | ); 41 | } 42 | 43 | const configFile = this.destinationPath('config', 'default.json'); 44 | 45 | if (this.fs.exists(configFile)) { 46 | const config = this.fs.readJSON(configFile); 47 | 48 | config.authentication = { 49 | 'entity': 'user', 50 | 'service': 'users', 51 | 'secret': crypto.randomBytes(20).toString('base64'), 52 | 'authStrategies': ['jwt', 'local'], 53 | 'jwtOptions': { 54 | 'header': { 'typ': 'access' }, 55 | 'audience': 'https://yourdomain.com', 56 | 'issuer': 'feathers', 57 | 'algorithm': 'HS256', 58 | 'expiresIn': '1d' 59 | }, 60 | 'local': { 61 | 'usernameField': 'email', 62 | 'passwordField': 'password' 63 | } 64 | }; 65 | 66 | this.fs.copy( 67 | configFile, 68 | this.destinationPath('config', 'default.backup.json') 69 | ); 70 | 71 | this.fs.writeJSON(configFile, config); 72 | } 73 | 74 | this.fs.writeJSON(this.destinationPath('package.json'), this.pkg); 75 | this._packagerInstall(dependencies, { 76 | save: true 77 | }); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /generators/app/templates/js/src/channels.js: -------------------------------------------------------------------------------- 1 | module.exports = function(app) { 2 | if(typeof app.channel !== 'function') { 3 | // If no real-time functionality has been configured just return 4 | return; 5 | } 6 | 7 | app.on('connection', connection => { 8 | // On a new real-time connection, add it to the anonymous channel 9 | app.channel('anonymous').join(connection); 10 | }); 11 | 12 | app.on('login', (authResult, { connection }) => { 13 | // connection can be undefined if there is no 14 | // real-time connection, e.g. when logging in via REST 15 | if(connection) { 16 | // Obtain the logged in user from the connection 17 | // const user = connection.user; 18 | 19 | // The connection is no longer anonymous, remove it 20 | app.channel('anonymous').leave(connection); 21 | 22 | // Add it to the authenticated user channel 23 | app.channel('authenticated').join(connection); 24 | 25 | // Channels can be named anything and joined on any condition 26 | 27 | // E.g. to send real-time events only to admins use 28 | // if(user.isAdmin) { app.channel('admins').join(connection); } 29 | 30 | // If the user has joined e.g. chat rooms 31 | // if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(connection)); 32 | 33 | // Easily organize users by email and userid for things like messaging 34 | // app.channel(`emails/${user.email}`).join(connection); 35 | // app.channel(`userIds/${user.id}`).join(connection); 36 | } 37 | }); 38 | 39 | // eslint-disable-next-line no-unused-vars 40 | app.publish((data, hook) => { 41 | // Here you can add event publishers to channels set up in `channels.js` 42 | // To publish only for a specific event use `app.publish(eventname, () => {})` 43 | 44 | console.log('Publishing all events to all authenticated users. See `channels.js` and https://docs.feathersjs.com/api/channels.html for more information.'); // eslint-disable-line 45 | 46 | // e.g. to publish all service events to all authenticated users use 47 | return app.channel('authenticated'); 48 | }); 49 | 50 | // Here you can also add service specific event publishers 51 | // e.g. the publish the `users` service `created` event to the `admins` channel 52 | // app.service('users').publish('created', () => app.channel('admins')); 53 | 54 | // With the userid and email organization from above you can easily select involved users 55 | // app.service('messages').publish(() => { 56 | // return [ 57 | // app.channel(`userIds/${data.createdBy}`), 58 | // app.channel(`emails/${data.recipientEmail}`) 59 | // ]; 60 | // }); 61 | }; 62 | -------------------------------------------------------------------------------- /test/generators.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const helpers = require('yeoman-test'); 3 | const assert = require('yeoman-assert'); 4 | 5 | const { startAndWait } = require('./utils'); 6 | 7 | describe('generator-feathers', function() { 8 | const tester = process.env.GENERATOR_TESTER || 'mocha'; 9 | 10 | let appDir; 11 | 12 | this.timeout(300000); 13 | 14 | function npmTest(expectedText) { 15 | return startAndWait('npm', ['test'], { cwd: appDir }) 16 | .then(({ buffer }) => { 17 | if (tester === 'mocha') { 18 | assert.ok(buffer.indexOf(expectedText) !== -1, 19 | 'Ran test with text: ' + expectedText); 20 | } 21 | }); 22 | } 23 | 24 | const runTest = adapter => () => { 25 | const prompts = { 26 | name: 'myapp', 27 | language: process.env.GENERATOR_LANGUAGE || 'js', 28 | authentication: true, 29 | providers: ['rest', 'socketio'], 30 | packager: 'npm', 31 | src: 'src', 32 | tester, 33 | adapter, 34 | strategies: ['local'] 35 | }; 36 | 37 | if (adapter === 'sequelize' || adapter === 'knex' || adapter === 'objection') { 38 | prompts.database = 'sqlite'; 39 | } 40 | 41 | before(() => helpers.run(path.join(__dirname, '..', 'generators', 'app')) 42 | .inTmpDir(dir => (appDir = dir)) 43 | .withPrompts(prompts) 44 | .withOptions({ 45 | skipInstall: false 46 | }) 47 | ); 48 | 49 | it('basic app tests', () => npmTest('starts and shows the index page')); 50 | 51 | it('feathers:hook', async () => { 52 | await helpers.run(path.join(__dirname, '../generators/hook')) 53 | .inTmpDir(function() { 54 | process.chdir(appDir); 55 | }) 56 | .withPrompts({ 57 | name: 'removeId', 58 | type: 'before', 59 | services: [ 'users' ], 60 | methods: [ 'create' ] 61 | }); 62 | 63 | await npmTest('starts and shows the index page'); 64 | }); 65 | 66 | it('feathers:middleware', async () => { 67 | await helpers.run(path.join(__dirname, '../generators/middleware')) 68 | .inTmpDir(function() { 69 | process.chdir(appDir); 70 | }) 71 | .withPrompts({ 72 | name: 'testmiddleware', 73 | path: '*' 74 | }); 75 | }); 76 | }; 77 | 78 | describe('with memory adapter', runTest('memory')); 79 | describe('with sequelize adapter', runTest('sequelize')); 80 | describe('with knex adapter', runTest('knex')); 81 | describe.skip('with objection adapter', runTest('objection')); 82 | 83 | describe('with mongoose adapter', runTest('mongoose')); 84 | 85 | // Needs to be skipped for now because Jest is finicky 86 | (tester === 'jest' ? describe.skip : describe)('with nedb adapter', runTest('nedb')); 87 | (tester === 'jest' ? describe.skip : describe)('with mongodb adapter', runTest('mongodb')); 88 | }); 89 | -------------------------------------------------------------------------------- /generators/app/templates/ts/src/channels.ts: -------------------------------------------------------------------------------- 1 | import '@feathersjs/transport-commons'; 2 | import { HookContext } from '@feathersjs/feathers'; 3 | import { Application } from './declarations'; 4 | 5 | export default function(app: Application): void { 6 | if(typeof app.channel !== 'function') { 7 | // If no real-time functionality has been configured just return 8 | return; 9 | } 10 | 11 | app.on('connection', (connection: any): void => { 12 | // On a new real-time connection, add it to the anonymous channel 13 | app.channel('anonymous').join(connection); 14 | }); 15 | 16 | app.on('login', (authResult: any, { connection }: any): void => { 17 | // connection can be undefined if there is no 18 | // real-time connection, e.g. when logging in via REST 19 | if(connection) { 20 | // Obtain the logged in user from the connection 21 | // const user = connection.user; 22 | 23 | // The connection is no longer anonymous, remove it 24 | app.channel('anonymous').leave(connection); 25 | 26 | // Add it to the authenticated user channel 27 | app.channel('authenticated').join(connection); 28 | 29 | // Channels can be named anything and joined on any condition 30 | 31 | // E.g. to send real-time events only to admins use 32 | // if(user.isAdmin) { app.channel('admins').join(connection); } 33 | 34 | // If the user has joined e.g. chat rooms 35 | // if(Array.isArray(user.rooms)) user.rooms.forEach(room => app.channel(`rooms/${room.id}`).join(connection)); 36 | 37 | // Easily organize users by email and userid for things like messaging 38 | // app.channel(`emails/${user.email}`).join(connection); 39 | // app.channel(`userIds/${user.id}`).join(connection); 40 | } 41 | }); 42 | 43 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 44 | app.publish((data: any, hook: HookContext) => { 45 | // Here you can add event publishers to channels set up in `channels.ts` 46 | // To publish only for a specific event use `app.publish(eventname, () => {})` 47 | 48 | console.log('Publishing all events to all authenticated users. See `channels.ts` and https://docs.feathersjs.com/api/channels.html for more information.'); // eslint-disable-line 49 | 50 | // e.g. to publish all service events to all authenticated users use 51 | return app.channel('authenticated'); 52 | }); 53 | 54 | // Here you can also add service specific event publishers 55 | // e.g. the publish the `users` service `created` event to the `admins` channel 56 | // app.service('users').publish('created', () => app.channel('admins')); 57 | 58 | // With the userid and email organization from above you can easily select involved users 59 | // app.service('messages').publish(() => { 60 | // return [ 61 | // app.channel(`userIds/${data.createdBy}`), 62 | // app.channel(`emails/${data.recipientEmail}`) 63 | // ]; 64 | // }); 65 | }; 66 | -------------------------------------------------------------------------------- /generators/middleware/index.js: -------------------------------------------------------------------------------- 1 | const { kebabCase, camelCase } = require('lodash'); 2 | const { transform, ts } = require('@feathersjs/tools'); 3 | const validate = require('validate-npm-package-name'); 4 | const Generator = require('../../lib/generator'); 5 | 6 | module.exports = class MiddlewareGenerator extends Generator { 7 | prompting () { 8 | this.checkPackage(); 9 | 10 | const prompts = [ 11 | { 12 | name: 'name', 13 | message: 'What is the name of the Express middleware?' 14 | }, 15 | { 16 | name: 'path', 17 | message: 'What is the mount path?', 18 | default: '*' 19 | } 20 | ]; 21 | 22 | return this.prompt(prompts).then(props => { 23 | this.props = Object.assign(this.props, props, { 24 | kebabName: validate(props.name).validForNewPackages ? props.name : kebabCase(props.name), 25 | camelName: camelCase(props.name) 26 | }); 27 | }); 28 | } 29 | 30 | _transformCode (code) { 31 | const { props } = this; 32 | const ast = transform(code); 33 | const mainExpression = ast.find(transform.FunctionExpression) 34 | .closest(transform.ExpressionStatement); 35 | 36 | if (mainExpression.length !== 1) { 37 | throw new Error(`${this.libDirectory}/middleware/index.js seems to have more than one function declaration and we can not register the new middleware. Did you modify it?`); 38 | } 39 | 40 | const middlewareRequire = `const ${props.camelName} = require('./${props.kebabName}');`; 41 | const middlewareCode = props.path === '*' ? `app.use(${props.camelName}());` : `app.use('${props.path}', ${props.camelName}());`; 42 | 43 | mainExpression.insertBefore(middlewareRequire); 44 | mainExpression.insertLastInFunction(middlewareCode); 45 | 46 | return ast.toSource(); 47 | } 48 | 49 | _transformCodeTs (code) { 50 | const { props } = this; 51 | const ast = transform(code, { 52 | parser: ts 53 | }); 54 | 55 | const middlewareImport = `import ${props.camelName} from './${props.kebabName}';`; 56 | const middlewareCode = props.path === '*' ? `app.use(${props.camelName}());` : `app.use('${props.path}', ${props.camelName}());`; 57 | 58 | const lastImport = ast.find(transform.ImportDeclaration).at(-1).get(); 59 | const newImport = transform(middlewareImport).find(transform.ImportDeclaration).get().node; 60 | lastImport.insertAfter(newImport); 61 | const blockStatement = ast.find(transform.BlockStatement).get().node; 62 | const newCode = transform(middlewareCode).find(transform.ExpressionStatement).get().node; 63 | blockStatement.body.push(newCode); 64 | 65 | return ast.toSource(); 66 | } 67 | 68 | writing () { 69 | const context = this.props; 70 | const mainFile = this.srcDestinationPath(this.libDirectory, 'middleware', context.kebabName); 71 | 72 | // Do not run code transformations if the middleware file already exists 73 | if (!this.fs.exists(mainFile)) { 74 | if (this.isTypescript) { 75 | const middlewarets = this.destinationPath(this.libDirectory, 'middleware', 'index.ts'); 76 | const transformed = this._transformCodeTs( 77 | this.fs.read(middlewarets).toString() 78 | ); 79 | 80 | this.conflicter.force = true; 81 | this.fs.write(middlewarets, transformed); 82 | } else { 83 | const middlewarejs = this.destinationPath(this.libDirectory, 'middleware', 'index.js'); 84 | const transformed = this._transformCode( 85 | this.fs.read(middlewarejs).toString() 86 | ); 87 | 88 | this.conflicter.force = true; 89 | this.fs.write(middlewarejs, transformed); 90 | } 91 | } 92 | 93 | this.fs.copyTpl( 94 | this.srcTemplatePath('middleware'), 95 | mainFile, context 96 | ); 97 | } 98 | }; 99 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generator-feathers", 3 | "description": "A Yeoman generator for a Feathers application", 4 | "version": "4.7.0", 5 | "homepage": "https://github.com/feathersjs/generator-feathers", 6 | "main": "generators/app/index.js", 7 | "license": "MIT", 8 | "keywords": [ 9 | "feathers", 10 | "feathers-plugin", 11 | "feathers-app-generator", 12 | "yeoman-generator", 13 | "yeoman" 14 | ], 15 | "licenses": [ 16 | { 17 | "type": "MIT", 18 | "url": "https://github.com/feathersjs/generator-feathers/blob/master/LICENSE" 19 | } 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/feathersjs/generator-feathers.git" 24 | }, 25 | "author": { 26 | "name": "Feathers contributors", 27 | "email": "hello@feathersjs.com", 28 | "url": "https://feathersjs.com" 29 | }, 30 | "contributors": [], 31 | "bugs": { 32 | "url": "https://github.com/feathersjs/generator-feathers/issues" 33 | }, 34 | "engines": { 35 | "node": ">= 12.0.0" 36 | }, 37 | "scripts": { 38 | "publish": "git push origin --tags && npm run changelog && git push origin", 39 | "release:pre": "npm version prerelease && npm publish --tag pre", 40 | "release:patch": "npm version patch && npm publish", 41 | "release:minor": "npm version minor && npm publish", 42 | "release:major": "npm version major && npm publish", 43 | "changelog": "github_changelog_generator -u feathersjs -p generator-feathers && git add CHANGELOG.md && git commit -am \"Updating changelog\"", 44 | "eslint": "eslint lib/. generators/**/index.js test/. --config .eslintrc.json", 45 | "test": "npm run eslint && npm run mocha", 46 | "mocha": "mocha test/generators.test.js --timeout 300000 --exit", 47 | "update-dependencies": "ncu -u -x yeoman-generator,yeoman-test" 48 | }, 49 | "dependencies": { 50 | "@feathersjs/tools": "^0.2.3", 51 | "colors": "1.4.0", 52 | "lodash": "^4.17.21", 53 | "node-dir": "^0.1.17", 54 | "randomstring": "^1.2.2", 55 | "semver": "^7.3.7", 56 | "validate-npm-package-name": "^4.0.0", 57 | "yeoman-generator": "^4.13.0" 58 | }, 59 | "devDependencies": { 60 | "@feathersjs/authentication": "^4.5.15", 61 | "@feathersjs/authentication-local": "^4.5.15", 62 | "@feathersjs/authentication-oauth": "^4.5.15", 63 | "@feathersjs/configuration": "^4.5.15", 64 | "@feathersjs/errors": "^4.5.15", 65 | "@feathersjs/express": "^4.5.15", 66 | "@feathersjs/feathers": "^4.5.15", 67 | "@feathersjs/primus": "^4.5.15", 68 | "@feathersjs/socketio": "^4.5.15", 69 | "@prisma/client": "^3.15.1", 70 | "@seald-io/nedb": "^3.0.0", 71 | "body-parser": "^1.20.0", 72 | "cassanknex": "^1.21.0", 73 | "compression": "^1.7.4", 74 | "cors": "^2.8.5", 75 | "debug": "^4.3.4", 76 | "eslint": "^8.17.0", 77 | "express-cassandra": "^2.8.0", 78 | "feathers-cassandra": "^3.5.8", 79 | "feathers-knex": "^8.0.1", 80 | "feathers-memory": "^4.1.0", 81 | "feathers-mongodb": "^6.4.1", 82 | "feathers-mongoose": "^8.5.1", 83 | "feathers-nedb": "^6.0.0", 84 | "feathers-objection": "^7.5.3", 85 | "feathers-prisma": "^0.5.8", 86 | "feathers-sequelize": "^6.3.4", 87 | "helmet": "^5.1.0", 88 | "jshint": "^2.13.4", 89 | "mocha": "^10.0.0", 90 | "mongodb": "^4.7.0", 91 | "mongoose": "^6.3.6", 92 | "nodemon": "^2.0.16", 93 | "npm-check-updates": "^13.1.5", 94 | "objection": "^3.0.1", 95 | "sequelize": "^6.20.1", 96 | "serve-favicon": "^2.5.0", 97 | "sqlite3": "^5.0.8", 98 | "typescript": "^4.7.3", 99 | "winston": "^3.7.2", 100 | "yeoman-assert": "^3.1.1", 101 | "yeoman-test": "^4.0.2" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /lib/generator.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Generator = require('yeoman-generator'); 3 | const semver = require('semver'); 4 | const _ = require('lodash'); 5 | const debug = require('debug')('generator-feathers'); 6 | 7 | module.exports = class BaseGenerator extends Generator { 8 | constructor(args, opts) { 9 | super(args, opts); 10 | 11 | this.generatorPkg = this.fs.readJSON(path.join(__dirname, '..', 'package.json')); 12 | this.pkg = this.fs.readJSON(this.destinationPath('package.json'), {}); 13 | this.defaultConfig = this.fs.readJSON(this.destinationPath(this.configDirectory, 'default.json'), {}); 14 | this.props = opts.props || {}; 15 | 16 | if (!semver.satisfies(process.version, '>= 8.0.0')) { 17 | this.log.error('The generator will only work with Node v8.0.0 and up!'); 18 | process.exit(); 19 | } 20 | 21 | if(this.pkg.dependencies && this.pkg.dependencies.feathers) { 22 | this.log.error('This version of the generator will only work with Feathers Buzzard (v3) and up. Please run `feathers upgrade` first.'); 23 | process.exit(); 24 | } 25 | } 26 | 27 | checkPackage() { 28 | if(_.isEmpty(this.pkg)) { 29 | this.log.error('Could not find a valid package.json. Did you generate a new application and are running the generator in the project directory?'); 30 | return process.exit(1); 31 | } 32 | 33 | if(!(this.pkg.directories && this.pkg.directories.lib)) { 34 | this.log.error('It does not look like this application has been generated with this version of the generator or the required `directories.lib` has been removed from package.json.'); 35 | return process.exit(1); 36 | } 37 | } 38 | 39 | get testLibrary () { 40 | return this.props.tester || (this.pkg.devDependencies.jest ? 'jest' : 'mocha'); 41 | } 42 | 43 | get libDirectory() { 44 | return this.pkg.directories && this.pkg.directories.lib; 45 | } 46 | 47 | get testDirectory() { 48 | return (this.pkg.directories && this.pkg.directories.test) || 'test'; 49 | } 50 | 51 | get configDirectory () { 52 | return (this.pkg.directories && this.pkg.directories.config) || 'config'; 53 | } 54 | 55 | get isTypescript () { 56 | const pkg = this.fs.readJSON(this.destinationPath('package.json')); 57 | const configFile = this.destinationPath('config', 'default.json'); 58 | 59 | let hasTS = pkg && pkg.types; 60 | 61 | if (!hasTS && this.fs.exists(configFile)) { 62 | const config = this.fs.readJSON(configFile); 63 | 64 | hasTS = config && config.ts; 65 | } 66 | 67 | return hasTS || (this.props.language === 'ts'); 68 | } 69 | 70 | get srcType () { 71 | return this.isTypescript ? 'ts' : 'js'; 72 | } 73 | 74 | srcTemplatePath (... names) { 75 | const name = `${names.pop()}.${this.srcType}`; 76 | 77 | return this.templatePath(...names, name); 78 | } 79 | 80 | srcDestinationPath (...names) { 81 | const name = `${names.pop()}.${this.srcType}`; 82 | 83 | return this.destinationPath(...names, name); 84 | } 85 | 86 | templatePath (...paths) { 87 | return super.templatePath(this.srcType, ...paths); 88 | } 89 | 90 | _packagerInstall(deps, options) { 91 | const packager = this.pkg.engines && this.pkg.engines.yarn ? 92 | 'yarn' : 'npm'; 93 | const method = `${packager}Install`; 94 | const isDev = options.saveDev; 95 | const existingDependencies = this.pkg[isDev ? 'devDependencies' : 'dependencies'] || {}; 96 | const dependencies = deps.filter(current => !existingDependencies[current]) 97 | .map(dependency => { 98 | const version = this.generatorPkg.devDependencies[dependency]; 99 | 100 | if(!version) { 101 | debug(`No locked version found for ${dependency}, installing latest.`); 102 | 103 | return dependency; 104 | } 105 | 106 | return `${dependency}@${version}`; 107 | }); 108 | 109 | if(packager === 'yarn' && isDev) { 110 | options.dev = true; 111 | delete options.saveDev; 112 | } 113 | 114 | return this[method](dependencies, options); 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /generators/hook/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const j = require('@feathersjs/tools').transform; 3 | const { kebabCase, camelCase, last } = require('lodash'); 4 | const dir = require('node-dir'); 5 | const validate = require('validate-npm-package-name'); 6 | const Generator = require('../../lib/generator'); 7 | 8 | module.exports = class HookGenerator extends Generator { 9 | _listServices (...args) { 10 | const serviceDir = this.destinationPath(...args); 11 | const files = dir.files(serviceDir, { sync: true }); 12 | const isTs = this.isTypescript; 13 | const services = files.filter(file => file.endsWith(isTs ? 'service.ts' : '.service.js')) 14 | .map(file => path.dirname(path.relative(serviceDir, file))); 15 | 16 | return services; 17 | } 18 | 19 | _transformHookFile (code, moduleName) { 20 | const { type, methods, camelName } = this.props; 21 | const hookRequire = `const ${camelName} = require('${moduleName}');`; 22 | 23 | const ast = j(code); 24 | const hookDefinitions = ast.find(j.ObjectExpression) 25 | .closest(j.ExpressionStatement); 26 | 27 | if (hookDefinitions.length !== 1) { 28 | throw new Error(`Could not find the hooks definition object while adding ${moduleName}`); 29 | } 30 | 31 | hookDefinitions.insertBefore(hookRequire); 32 | 33 | methods.forEach(method => { 34 | ast.insertHook(type, method, camelName); 35 | }); 36 | 37 | return ast.toSource(); 38 | } 39 | 40 | _transformHookFileTs (code, moduleName) { 41 | const { type, methods, camelName } = this.props; 42 | const hookImport = `import ${camelName} from '${moduleName}';`; 43 | 44 | const ast = j(code); 45 | const hookDefinitions = ast.find(j.ExportDefaultDeclaration); 46 | 47 | if (hookDefinitions.length !== 1) { 48 | throw new Error(`Could not find the hooks definition object while adding ${moduleName}`); 49 | } 50 | 51 | const imports = ast.find(j.ImportDeclaration); 52 | if (imports.length === 0) { 53 | const newImport = j(hookImport).find(j.ImportDeclaration).get().node; 54 | hookDefinitions.insertBefore(newImport); 55 | } else{ 56 | const lastImport = ast.find(j.ImportDeclaration).at(-1).get(); 57 | const newImport = j(hookImport).find(j.ImportDeclaration).get().node; 58 | lastImport.insertAfter(newImport); 59 | } 60 | 61 | methods.forEach(method => { 62 | ast.insertHook(type, method, camelName); 63 | }); 64 | 65 | return ast.toSource(); 66 | } 67 | 68 | _addToService (serviceName, hookName) { 69 | const nameParts = serviceName.split('/'); 70 | const relativeRoot = '../'.repeat(nameParts.length + 1); 71 | 72 | let hooksFile = this.srcDestinationPath(this.libDirectory, 'services', ...nameParts, `${last(nameParts)}.hooks`); 73 | let moduleName = relativeRoot + hookName; 74 | 75 | if (serviceName === '__app') { 76 | hooksFile = this.srcDestinationPath(this.libDirectory, 'app.hooks'); 77 | moduleName = `./${hookName}`; 78 | } 79 | 80 | if (!this.fs.exists(hooksFile)) { 81 | throw new Error(`Can not add hook to the ${serviceName} hooks file ${hooksFile}. It does not exist.`); 82 | } 83 | 84 | const transformed = this.isTypescript ? this._transformHookFileTs(this.fs.read(hooksFile), moduleName) 85 | : this._transformHookFile(this.fs.read(hooksFile), moduleName); 86 | 87 | this.conflicter.force = true; 88 | this.fs.write(hooksFile, transformed); 89 | } 90 | 91 | prompting () { 92 | this.checkPackage(); 93 | 94 | const services = this._listServices(this.libDirectory, 'services'); 95 | const prompts = [ 96 | { 97 | name: 'name', 98 | message: 'What is the name of the hook?' 99 | }, { 100 | type: 'list', 101 | name: 'type', 102 | message: 'What kind of hook should it be?', 103 | choices: [ 104 | { 105 | name: 'I will add it myself', 106 | value: null 107 | }, { 108 | value: 'before' 109 | }, { 110 | value: 'after' 111 | }, { 112 | value: 'error' 113 | } 114 | ] 115 | }, { 116 | type: 'checkbox', 117 | name: 'services', 118 | message: 'What service(s) should this hook be for (select none to add it yourself)?\n', 119 | choices () { 120 | return [{ 121 | name: 'Application wide (all services)', 122 | value: '__app' 123 | }].concat(services.map(value => ({ value }))); 124 | }, 125 | when (answers) { 126 | return answers.type !== null; 127 | }, 128 | validate(answers) { 129 | if (answers.length < 1) { 130 | return 'You have to select at least one service (use Space key to select).'; 131 | } 132 | 133 | return true; 134 | } 135 | }, { 136 | type: 'checkbox', 137 | name: 'methods', 138 | message: 'What methods should the hook be for (select none to add it yourself)?', 139 | choices: [ 140 | { 141 | value: 'all' 142 | }, { 143 | value: 'find' 144 | }, { 145 | value: 'get' 146 | }, { 147 | value: 'create' 148 | }, { 149 | value: 'update' 150 | }, { 151 | value: 'patch' 152 | }, { 153 | value: 'remove' 154 | } 155 | ], 156 | when (answers) { 157 | return answers.type !== null && answers.services.length; 158 | }, 159 | validate (methods) { 160 | if (methods.length < 1) { 161 | return 'You have to select at least one method (use Space key to select).'; 162 | } 163 | 164 | if (methods.indexOf('all') !== -1 && methods.length !== 1) { 165 | return 'Select applicable methods or \'all\', not both.'; 166 | } 167 | 168 | return true; 169 | } 170 | } 171 | ]; 172 | 173 | return this.prompt(prompts).then(props => { 174 | this.props = Object.assign(this.props, props, { 175 | kebabName: validate(props.name).validForNewPackages ? props.name : kebabCase(props.name), 176 | camelName: camelCase(props.name) 177 | }); 178 | }); 179 | } 180 | 181 | writing () { 182 | const context = Object.assign({ 183 | libDirectory: this.libDirectory 184 | }, this.props); 185 | const mainFile = this.srcDestinationPath(this.libDirectory, 'hooks', context.kebabName); 186 | 187 | if (!this.fs.exists(mainFile) && context.type) { 188 | this.props.services.forEach(serviceName => 189 | this._addToService(serviceName, `hooks/${context.kebabName}`) 190 | ); 191 | } 192 | 193 | this.fs.copyTpl( 194 | this.srcTemplatePath('hook'), 195 | mainFile, context 196 | ); 197 | } 198 | }; 199 | -------------------------------------------------------------------------------- /generators/authentication/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const { transform, ts } = require('@feathersjs/tools'); 3 | const crypto = require('crypto'); 4 | const validate = require('validate-npm-package-name'); 5 | 6 | const Generator = require('../../lib/generator'); 7 | 8 | module.exports = class AuthGenerator extends Generator { 9 | prompting() { 10 | this.checkPackage(); 11 | 12 | const prompts = [{ 13 | type: 'checkbox', 14 | name: 'strategies', 15 | message: 'What authentication strategies do you want to use? (See API docs for all 180+ supported oAuth providers)', 16 | choices: [{ 17 | name: 'Username + Password (Local)', 18 | value: 'local', 19 | checked: true 20 | }, { 21 | name: 'Auth0', 22 | value: 'auth0' 23 | }, { 24 | name: 'Google', 25 | value: 'google' 26 | }, { 27 | name: 'Facebook', 28 | value: 'facebook' 29 | }, { 30 | name: 'Twitter', 31 | value: 'twitter' 32 | }, { 33 | name: 'GitHub', 34 | value: 'github' 35 | }] 36 | }, { 37 | name: 'entity', 38 | message: 'What is the name of the user (entity) service?', 39 | default: 'users' 40 | }]; 41 | 42 | return this.prompt(prompts).then(props => { 43 | this.props = Object.assign(this.props, props); 44 | this.props.oauthProviders = this.props.strategies.filter(s => s !== 'local'); 45 | }); 46 | } 47 | 48 | _transformCode(code) { 49 | const ast = transform(code); 50 | const appDeclaration = ast.findDeclaration('app'); 51 | const configureServices = ast.findConfigure('services'); 52 | const requireCall = 'const authentication = require(\'./authentication\');'; 53 | 54 | if (appDeclaration.length === 0) { 55 | throw new Error('Could not find \'app\' variable declaration in app.js to insert database configuration. Did you modify app.js?'); 56 | } 57 | 58 | if (configureServices.length === 0) { 59 | throw new Error('Could not find .configure(services) call in app.js after which to insert database configuration. Did you modify app.js?'); 60 | } 61 | 62 | appDeclaration.insertBefore(requireCall); 63 | configureServices.insertBefore('app.configure(authentication);'); 64 | 65 | return ast.toSource(); 66 | } 67 | 68 | _transformCodeTs(code) { 69 | const ast = transform(code, { 70 | parser: ts 71 | }); 72 | const appDeclaration = ast.findDeclaration('app'); 73 | const configureServices = ast.findConfigure('services'); 74 | const requireCall = 'import authentication from \'./authentication\';'; 75 | 76 | if (appDeclaration.length === 0) { 77 | throw new Error('Could not find \'app\' variable declaration in app.ts to insert database configuration. Did you modify app.ts?'); 78 | } 79 | 80 | if (configureServices.length === 0) { 81 | throw new Error('Could not find .configure(services) call in app.ts after which to insert database configuration. Did you modify app.ts?'); 82 | } 83 | 84 | appDeclaration.insertBefore(requireCall); 85 | configureServices.insertBefore('app.configure(authentication);'); 86 | 87 | return ast.toSource(); 88 | } 89 | 90 | _writeConfiguration(context) { 91 | const config = Object.assign({}, this.defaultConfig); 92 | 93 | const authentication = { 94 | 'entity': context.singularEntity, 95 | 'service': context.camelEntity, 96 | 'secret': crypto.randomBytes(20).toString('base64'), 97 | 'authStrategies': ['jwt', 'local'], 98 | 'jwtOptions': { 99 | 'header': { 'typ': 'access' }, 100 | 'audience': 'https://yourdomain.com', 101 | 'issuer': 'feathers', 102 | 'algorithm': 'HS256', 103 | 'expiresIn': '1d' 104 | }, 105 | 'local': { 106 | 'usernameField': 'email', 107 | 'passwordField': 'password' 108 | } 109 | }; 110 | 111 | const { oauthProviders = [] } = this.props; 112 | 113 | if (oauthProviders.length) { 114 | authentication.oauth = authentication.oauth || { 115 | redirect: '/' 116 | }; 117 | 118 | for (let strategy of this.props.oauthProviders) { 119 | const strategyConfig = { 120 | key: `<${strategy} oauth key>`, 121 | secret: `<${strategy} oauth secret>` 122 | }; 123 | 124 | switch (strategy) { 125 | case 'google': 126 | strategyConfig.scope = [ 'email', 'profile', 'openid' ]; 127 | break; 128 | 129 | case 'auth0': 130 | strategyConfig.subdomain = `<${strategy} subdomain>`; 131 | strategyConfig.scope = [ 'profile', 'openid', 'email' ]; 132 | break; 133 | } 134 | 135 | authentication.oauth[strategy] = strategyConfig; 136 | } 137 | } 138 | 139 | 140 | config.authentication = authentication; 141 | this.conflicter.force = true; 142 | this.fs.writeJSON( 143 | this.destinationPath(this.configDirectory, 'default.json'), 144 | config 145 | ); 146 | } 147 | 148 | writing() { 149 | const dependencies = [ 150 | '@feathersjs/authentication', 151 | '@feathersjs/authentication-local', 152 | '@feathersjs/authentication-oauth' 153 | ]; 154 | const context = Object.assign({ 155 | kebabEntity: validate(this.props.entity).validForNewPackages ? this.props.entity : _.kebabCase(this.props.entity), 156 | camelEntity: _.camelCase(this.props.entity), 157 | singularEntity: _.camelCase(this.props.entity).replace(/s$/, ''), 158 | libDirectory: this.libDirectory 159 | }, this.props); 160 | 161 | if(!this.fs.exists(this.srcDestinationPath(this.libDirectory, 'services', context.kebabEntity, `${context.kebabEntity}.service`))) { 162 | // Create the users service 163 | this.composeWith(require.resolve('../service'), { 164 | props: { 165 | tester: context.tester, 166 | name: context.entity, 167 | path: `/${context.kebabEntity}`, 168 | authentication: context 169 | } 170 | }); 171 | } 172 | 173 | // If the file doesn't exist yet, add it to the app.js 174 | if (!this.fs.exists(this.srcDestinationPath(this.libDirectory, 'authentication'))) { 175 | const appSrc = this.srcDestinationPath(this.libDirectory, 'app'); 176 | 177 | this.conflicter.force = true; 178 | const code = this.fs.read(appSrc).toString(); 179 | let transformed; 180 | if (this.srcType === 'ts') { 181 | transformed = this._transformCodeTs(code); 182 | } else { 183 | transformed = this._transformCode(code); 184 | } 185 | this.fs.write(appSrc, transformed); 186 | } 187 | 188 | this.fs.copyTpl( 189 | this.srcTemplatePath('authentication'), 190 | this.srcDestinationPath(this.libDirectory, 'authentication'), 191 | context 192 | ); 193 | 194 | this.fs.copyTpl( 195 | this.srcTemplatePath(`test.${this.testLibrary}`), 196 | this.srcDestinationPath(this.testDirectory, 'authentication.test'), 197 | context 198 | ); 199 | 200 | this._writeConfiguration(context); 201 | this._packagerInstall(dependencies, { 202 | save: true 203 | }); 204 | if (this.srcType === 'ts') { 205 | this._packagerInstall(['@types/jsonwebtoken'], { saveDev: true }); 206 | } 207 | } 208 | }; 209 | -------------------------------------------------------------------------------- /generators/service/index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const { transform, ts } = require('@feathersjs/tools'); 3 | const validate = require('validate-npm-package-name'); 4 | const Generator = require('../../lib/generator'); 5 | 6 | const stripSlashes = name => name.replace(/^(\/*)|(\/*)$/g, ''); 7 | 8 | module.exports = class ServiceGenerator extends Generator { 9 | prompting() { 10 | this.checkPackage(); 11 | 12 | const { props } = this; 13 | const prompts = [ 14 | { 15 | type: 'list', 16 | name: 'adapter', 17 | message: 'What kind of service is it?', 18 | default: 'nedb', 19 | choices: [ 20 | { name: 'A custom service', value: 'generic' }, 21 | { name: 'In Memory', value: 'memory' }, 22 | { name: 'NeDB', value: 'nedb' }, 23 | { name: 'MongoDB', value: 'mongodb' }, 24 | { name: 'Mongoose', value: 'mongoose' }, 25 | { name: 'Sequelize', value: 'sequelize' }, 26 | { name: 'KnexJS', value: 'knex' }, 27 | { name: 'Objection', value: 'objection' }, 28 | { name: 'Cassandra', value: 'cassandra' }, 29 | { name: 'Couchbase', value: 'couchbase' }, 30 | { name: 'Prisma', value: 'prisma' } 31 | ] 32 | }, { 33 | name: 'name', 34 | message: 'What is the name of the service?', 35 | validate(input) { 36 | switch (input.trim()) { 37 | case '': 38 | return 'Service name can not be empty'; 39 | case 'authentication': 40 | return '`authentication` is a reserved service name'; 41 | default: 42 | return true; 43 | } 44 | }, 45 | when: !props.name 46 | }, { 47 | name: 'path', 48 | message: 'Which path should the service be registered on?', 49 | when: !props.path, 50 | default(answers) { 51 | const parts = (answers.name || props.name).split('/'); 52 | const name = _.kebabCase(parts.pop()); 53 | 54 | return `/${parts.concat(name).join('/')}`; 55 | }, 56 | validate(input) { 57 | if (input.trim() === '') { 58 | return 'Service path can not be empty'; 59 | } 60 | 61 | return true; 62 | } 63 | }, { 64 | name: 'requiresAuth', 65 | message: 'Does the service require authentication?', 66 | type: 'confirm', 67 | default: true, 68 | when: !!(this.defaultConfig.authentication && !props.authentication) 69 | } 70 | ]; 71 | 72 | return this.prompt(prompts).then(answers => { 73 | const parts = (answers.name || props.name) 74 | .split('/') 75 | // exclude route parameters from folder hierarchy i.e. /users/:id/roles 76 | .filter(part => !part.startsWith(':')); 77 | const name = parts.pop(); 78 | 79 | this.props = Object.assign({ 80 | requiresAuth: false 81 | }, props, answers, { 82 | subfolder: parts, 83 | snakeName: _.snakeCase(name), 84 | kebabName: validate(name).validForNewPackages ? name : _.kebabCase(name), 85 | camelName: _.camelCase(name), 86 | className: _.upperFirst(_.camelCase(name)) 87 | }); 88 | }); 89 | } 90 | 91 | _transformCode(code) { 92 | const { kebabName, subfolder } = this.props; 93 | const ast = transform(code); 94 | const mainExpression = ast.find(transform.FunctionExpression).closest(transform.ExpressionStatement); 95 | const folder = subfolder.concat(kebabName).join('/'); 96 | const camelName = _.camelCase(folder); 97 | const serviceRequire = `const ${camelName} = require('./${folder}/${kebabName}.service.js');`; 98 | const serviceCode = `app.configure(${camelName});`; 99 | 100 | if (mainExpression.length !== 1) { 101 | this.log 102 | .writeln() 103 | .conflict(`${this.libDirectory}/services/index.js seems to have more than one function declaration and we can not register the new service. Did you modify it?`) 104 | .info('You will need to add the next lines manually to the file') 105 | .info(serviceRequire) 106 | .info(serviceCode) 107 | .writeln(); 108 | } else { 109 | // Add require('./service') 110 | mainExpression.insertBefore(serviceRequire); 111 | // Add app.configure(service) to service/index.js 112 | mainExpression.insertLastInFunction(serviceCode); 113 | } 114 | 115 | return ast.toSource(); 116 | } 117 | 118 | _transformCodeTs(code) { 119 | const { kebabName, subfolder } = this.props; 120 | const ast = transform(code, { 121 | parser: ts 122 | }); 123 | const folder = subfolder.concat(kebabName).join('/'); 124 | const camelName = _.camelCase(folder); 125 | const serviceImport = `import ${camelName} from './${folder}/${kebabName}.service';`; 126 | const serviceCode = `app.configure(${camelName});`; 127 | 128 | const lastImport = ast.find(transform.ImportDeclaration).at(-1).get(); 129 | const newImport = transform(serviceImport).find(transform.ImportDeclaration).get().node; 130 | 131 | lastImport.insertAfter(newImport); 132 | 133 | const blockStatement = ast.find(transform.BlockStatement).get().node; 134 | const newCode = transform(serviceCode).find(transform.ExpressionStatement).get().node; 135 | blockStatement.body.push(newCode); 136 | 137 | return ast.toSource(); 138 | } 139 | 140 | writing() { 141 | const { adapter, kebabName, subfolder } = this.props; 142 | const moduleMappings = { 143 | generic: `./${kebabName}.class`, 144 | memory: 'feathers-memory', 145 | nedb: 'feathers-nedb', 146 | mongodb: 'feathers-mongodb', 147 | mongoose: 'feathers-mongoose', 148 | sequelize: 'feathers-sequelize', 149 | knex: 'feathers-knex', 150 | objection: 'feathers-objection', 151 | cassandra: 'feathers-cassandra', 152 | couchbase: 'feathers-couchbase', 153 | prisma: 'feathers-prisma' 154 | }; 155 | const serviceModule = moduleMappings[adapter]; 156 | const serviceFolder = [this.libDirectory, 'services', ...subfolder, kebabName]; 157 | const mainFile = this.srcDestinationPath(...serviceFolder, `${kebabName}.service`); 158 | const modelTpl = `${adapter}${this.props.authentication ? '-user' : ''}`; 159 | const hasModel = this.fs.exists(this.srcTemplatePath('model', modelTpl)); 160 | const context = Object.assign({}, this.props, { 161 | libDirectory: this.libDirectory, 162 | modelName: hasModel ? `${kebabName}.model` : null, 163 | path: stripSlashes(this.props.path), 164 | relativeRoot: '../'.repeat(subfolder.length + 2), 165 | serviceModule 166 | }); 167 | 168 | // Do not run code transformations if the service file already exists 169 | if (!this.fs.exists(mainFile)) { 170 | const servicejs = this.srcDestinationPath(this.libDirectory, 'services', 'index'); 171 | let transformed; 172 | if (this.isTypescript) { 173 | transformed = this._transformCodeTs( 174 | this.fs.read(servicejs).toString() 175 | ); 176 | } else { 177 | transformed = this._transformCode( 178 | this.fs.read(servicejs).toString() 179 | ); 180 | } 181 | this.conflicter.force = true; 182 | this.fs.write(servicejs, transformed); 183 | } 184 | 185 | const requiresConnection = adapter !== 'generic' && adapter !== 'memory' && 186 | !this.fs.exists(this.srcDestinationPath(this.libDirectory, adapter)); 187 | 188 | // Run the `connection` generator for the selected database 189 | // It will not do anything if the db has been set up already 190 | if (requiresConnection) { 191 | this.composeWith(require.resolve('../connection'), { 192 | props: { adapter, service: this.props.name } 193 | }); 194 | } 195 | 196 | // Copy the service class 197 | this.fs.copyTpl( 198 | this.srcTemplatePath('types', adapter), 199 | this.srcDestinationPath(...serviceFolder, `${kebabName}.class`), 200 | context 201 | ); 202 | 203 | if (context.modelName) { 204 | // Copy the model 205 | this.fs.copyTpl( 206 | this.srcTemplatePath('model', modelTpl), 207 | this.srcDestinationPath(this.libDirectory, 'models', context.modelName), 208 | context 209 | ); 210 | } 211 | 212 | this.fs.copyTpl( 213 | this.srcTemplatePath(`hooks${this.props.authentication ? '-user' : ''}`), 214 | this.srcDestinationPath(...serviceFolder, `${kebabName}.hooks`), 215 | context 216 | ); 217 | 218 | this.fs.copyTpl( 219 | this.srcTemplatePath('service'), 220 | mainFile, 221 | context 222 | ); 223 | 224 | this.fs.copyTpl( 225 | this.srcTemplatePath(`test.${this.testLibrary}`), 226 | this.srcDestinationPath(this.testDirectory, 'services', ...subfolder, `${kebabName}.test`), 227 | context 228 | ); 229 | 230 | if (serviceModule.charAt(0) !== '.') { 231 | this._packagerInstall([serviceModule], { save: true }); 232 | } 233 | 234 | if (this.isTypescript) { 235 | const typeMap = { 236 | sequelize: [ '@types/bluebird' ], 237 | mongodb: ['@types/mongodb'], 238 | cassandra: ['@types/cassaknex'] 239 | }; 240 | 241 | if (typeMap[adapter]) { 242 | this._packagerInstall(typeMap[adapter], { saveDev: true }); 243 | } 244 | } 245 | } 246 | }; 247 | -------------------------------------------------------------------------------- /generators/app/index.js: -------------------------------------------------------------------------------- 1 | const Generator = require('../../lib/generator'); 2 | const path = require('path'); 3 | const makeConfig = require('./configs'); 4 | const { kebabCase } = require('lodash'); 5 | 6 | module.exports = class AppGenerator extends Generator { 7 | constructor (args, opts) { 8 | super(args, opts); 9 | 10 | this.props = { 11 | name: this.pkg.name || process.cwd().split(path.sep).pop(), 12 | description: this.pkg.description, 13 | src: this.pkg.directories && this.pkg.directories.lib 14 | }; 15 | 16 | this.dependencies = [ 17 | '@feathersjs/feathers', 18 | '@feathersjs/errors', 19 | '@feathersjs/configuration', 20 | '@feathersjs/express', 21 | '@feathersjs/transport-commons', 22 | 'serve-favicon', 23 | 'compression', 24 | 'helmet', 25 | 'winston@^3.0.0', 26 | 'cors' 27 | ]; 28 | 29 | this.devDependencies = [ 30 | 'nodemon', 31 | 'axios' 32 | ]; 33 | } 34 | 35 | prompting () { 36 | const dependencies = this.dependencies.concat(this.devDependencies) 37 | .concat([ 38 | '@feathersjs/express', 39 | '@feathersjs/socketio', 40 | '@feathersjs/primus' 41 | ]); 42 | const prompts = [{ 43 | type: 'list', 44 | name: 'language', 45 | message: 'Do you want to use JavaScript or TypeScript?', 46 | default: 'js', 47 | choices: [ 48 | { name: 'JavaScript', value: 'js' }, 49 | { name: 'TypeScript', value: 'ts' } 50 | ], 51 | }, { 52 | name: 'name', 53 | message: 'Project name', 54 | when: !this.pkg.name, 55 | default: this.props.name, 56 | filter: kebabCase, 57 | validate (input) { 58 | // The project name can not be the same as any of the dependencies 59 | // we are going to install 60 | const isSelfReferential = dependencies.some(dependency => { 61 | const separatorIndex = dependency.indexOf('@'); 62 | const end = separatorIndex !== -1 ? separatorIndex : dependency.length; 63 | const dependencyName = dependency.substring(0, end); 64 | 65 | return dependencyName === input; 66 | }); 67 | 68 | if (isSelfReferential) { 69 | return `Your project can not be named '${input}' because the '${input}' package will be installed as a project dependency.`; 70 | } 71 | 72 | return true; 73 | } 74 | }, { 75 | name: 'description', 76 | message: 'Description', 77 | when: !this.pkg.description 78 | }, { 79 | name: 'src', 80 | message: 'What folder should the source files live in?', 81 | default: 'src', 82 | when: !(this.pkg.directories && this.pkg.directories.lib) 83 | }, { 84 | name: 'packager', 85 | type: 'list', 86 | message: 'Which package manager are you using (has to be installed globally)?', 87 | default: 'npm', 88 | choices: [ 89 | { name: 'npm', value: 'npm@>= 3.0.0' }, 90 | { name: 'Yarn', value: 'yarn@>= 0.18.0' } 91 | ] 92 | }, { 93 | type: 'checkbox', 94 | name: 'providers', 95 | message: 'What type of API are you making?', 96 | choices: [ 97 | { name: 'REST', value: 'rest', checked: true }, 98 | { name: 'Realtime via Socket.io', value: 'socketio', checked: true }, 99 | { name: 'Realtime via Primus', value: 'primus', } 100 | ], 101 | validate (input) { 102 | if (input.indexOf('primus') !== -1 && input.indexOf('socketio') !== -1) { 103 | return 'You can only pick SocketIO or Primus, not both.'; 104 | } 105 | 106 | return true; 107 | } 108 | }, { 109 | type: 'list', 110 | name: 'tester', 111 | message: 'Which testing framework do you prefer?', 112 | default: 'mocha', 113 | choices: [ 114 | { name: 'Mocha + assert', value: 'mocha' }, 115 | { name: 'Jest', value: 'jest' } 116 | ] 117 | }, { 118 | name: 'authentication', 119 | message: 'This app uses authentication', 120 | type: 'confirm', 121 | default: true 122 | }]; 123 | 124 | const jsPrompts = [{ 125 | type: 'list', 126 | name: 'linter', 127 | message: 'Which coding style do you want to use?', 128 | default: 'eslint', 129 | choices: [ 130 | { name: 'ESLint', value: 'eslint' }, 131 | { name: 'StandardJS', value: 'standard' } 132 | ], 133 | }]; 134 | 135 | return this.prompt(prompts).then(props => { 136 | props = Object.assign({}, this.props, props, { 137 | linter: 'eslint' 138 | }); 139 | 140 | if (props.language === 'js') { 141 | return this.prompt(jsPrompts).then(jsProps => { 142 | return Object.assign({}, props, jsProps); 143 | }); 144 | } else { 145 | return props; 146 | } 147 | }).then(props => { 148 | this.props = Object.assign({}, this.props, props); 149 | }); 150 | } 151 | 152 | writing () { 153 | const props = this.props; 154 | const pkg = this.pkg = makeConfig.package(this); 155 | const context = Object.assign({}, props, { 156 | hasProvider (name) { 157 | return props.providers.indexOf(name) !== -1; 158 | } 159 | }); 160 | 161 | // Static content for the root folder (including dotfiles) 162 | this.fs.copy(this.templatePath('..', 'static'), this.destinationPath()); 163 | this.fs.copy(this.templatePath('..', 'static', '.*'), this.destinationPath()); 164 | // Static content for the directories.lib folder 165 | this.fs.copy(this.templatePath('src'), this.destinationPath(props.src)); 166 | // This hack is necessary because NPM does not publish `.gitignore` files 167 | this.fs.copy(this.templatePath('_gitignore'), this.destinationPath('', '.gitignore')); 168 | 169 | this.fs.copyTpl( 170 | this.templatePath('README.md'), 171 | this.destinationPath('', 'README.md'), 172 | context 173 | ); 174 | 175 | this.fs.copyTpl( 176 | this.srcTemplatePath('app'), 177 | this.srcDestinationPath(this.libDirectory, 'app'), 178 | context 179 | ); 180 | 181 | this.fs.copyTpl( 182 | this.srcTemplatePath(`app.test.${props.tester}`), 183 | this.srcDestinationPath(this.testDirectory, 'app.test'), 184 | context 185 | ); 186 | 187 | this.fs.writeJSON( 188 | this.destinationPath('package.json'), 189 | pkg 190 | ); 191 | 192 | if (this.isTypescript) { 193 | this.fs.writeJSON( 194 | this.destinationPath('tsconfig.json'), 195 | makeConfig.tsconfig(this) 196 | ); 197 | 198 | if (props.tester === 'jest') { 199 | this.fs.copyTpl( 200 | this.templatePath('jest.config.js'), 201 | this.destinationPath('jest.config.js'), 202 | context 203 | ); 204 | } 205 | } 206 | 207 | if (props.linter === 'eslint') { 208 | this.fs.writeJSON( 209 | this.destinationPath('.eslintrc.json'), 210 | makeConfig.eslintrc(this) 211 | ); 212 | } 213 | 214 | this.fs.writeJSON( 215 | this.destinationPath(this.configDirectory, 'default.json'), 216 | makeConfig.configDefault(this) 217 | ); 218 | 219 | this.fs.writeJSON( 220 | this.destinationPath(this.configDirectory, 'production.json'), 221 | makeConfig.configProduction(this) 222 | ); 223 | 224 | this.fs.writeJSON( 225 | this.destinationPath(this.configDirectory, 'test.json'), 226 | makeConfig.configTest(this) 227 | ); 228 | 229 | if (props.authentication) { 230 | // Create the users service 231 | this.composeWith(require.resolve('../authentication'), { 232 | props: { tester: props.tester } 233 | }); 234 | } 235 | } 236 | 237 | install () { 238 | this.props.providers.forEach(provider => { 239 | const type = provider === 'rest' ? 'express' : provider; 240 | 241 | this.dependencies.push(`@feathersjs/${type}`); 242 | 243 | if (provider === 'primus') { 244 | this.dependencies.push('ws'); 245 | } 246 | }); 247 | 248 | this._packagerInstall(this.dependencies, { 249 | save: true 250 | }); 251 | 252 | if (this.isTypescript) { 253 | const excluded = [ 254 | 'nodemon', 255 | ]; 256 | this.devDependencies = this.devDependencies.concat([ 257 | '@types/compression', 258 | '@types/cors', 259 | '@types/serve-favicon', 260 | 'shx', 261 | 'ts-node-dev', 262 | 'typescript', 263 | `@types/${this.props.tester}` 264 | ]).filter(item => !excluded.includes(item)); 265 | 266 | if (this.props.tester === 'jest') { 267 | this.devDependencies.push('ts-jest'); 268 | } 269 | 270 | this.devDependencies = this.devDependencies.concat([ 271 | this.props.linter, 272 | '@typescript-eslint/eslint-plugin', 273 | '@typescript-eslint/parser', 274 | ]); 275 | } else { 276 | this.devDependencies.push(this.props.linter); 277 | } 278 | 279 | this.devDependencies.push(this.props.tester); 280 | 281 | this._packagerInstall(this.devDependencies, { 282 | saveDev: true 283 | }); 284 | 285 | } 286 | 287 | end () { 288 | if (this.isTypescript && this.props.linter !== 'eslint') return; 289 | 290 | const [ packager, ] = this.props.packager.split('@'); 291 | 292 | if (packager === 'yarn') { 293 | this.spawnCommand(packager, ['run', 'lint', '--fix']); 294 | } else { 295 | this.spawnCommand(packager, ['run', 'lint', '--', '--fix']); 296 | } 297 | } 298 | }; 299 | -------------------------------------------------------------------------------- /generators/app/templates/static/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A FeathersJS application 5 | 6 | 7 | 65 | 66 | 67 |
68 | 69 | 70 | 73 |
74 | 75 | 76 | -------------------------------------------------------------------------------- /generators/connection/index.js: -------------------------------------------------------------------------------- 1 | const { snakeCase } = require('lodash'); 2 | const url = require('url'); 3 | const { transform, ts } = require('@feathersjs/tools'); 4 | const Generator = require('../../lib/generator'); 5 | 6 | module.exports = class ConnectionGenerator extends Generator { 7 | constructor (args, opts) { 8 | super(args, opts); 9 | 10 | this.dependencies = []; 11 | this.devDependencies = []; 12 | } 13 | 14 | _transformCode (code) { 15 | const { adapter } = this.props; 16 | 17 | const ast = transform(code); 18 | const appDeclaration = ast.findDeclaration('app'); 19 | const configureMiddleware = ast.findConfigure('middleware'); 20 | const requireCall = `const ${adapter} = require('./${adapter}');`; 21 | 22 | if (appDeclaration.length === 0) { 23 | throw new Error('Could not find \'app\' variable declaration in app.js to insert database configuration. Did you modify app.js?'); 24 | } 25 | 26 | if (configureMiddleware.length === 0) { 27 | throw new Error('Could not find .configure(middleware) call in app.js after which to insert database configuration. Did you modify app.js?'); 28 | } 29 | 30 | appDeclaration.insertBefore(requireCall); 31 | configureMiddleware.insertBefore(`app.configure(${adapter});`); 32 | 33 | return ast.toSource(); 34 | } 35 | 36 | _transformCodeTs (code) { 37 | const { adapter } = this.props; 38 | 39 | const ast = transform(code, { 40 | parser: ts 41 | }); 42 | const appDeclaration = ast.findDeclaration('app'); 43 | const configureMiddleware = ast.findConfigure('middleware'); 44 | const requireCall = `import ${adapter} from './${adapter}';`; 45 | 46 | if (appDeclaration.length === 0) { 47 | throw new Error('Could not find \'app\' variable declaration in app.ts to insert database configuration. Did you modify app.js?'); 48 | } 49 | 50 | if (configureMiddleware.length === 0) { 51 | throw new Error('Could not find .configure(middleware) call in app.ts after which to insert database configuration. Did you modify app.js?'); 52 | } 53 | 54 | appDeclaration.insertBefore(requireCall); 55 | configureMiddleware.insertBefore(`app.configure(${adapter});`); 56 | 57 | return ast.toSource(); 58 | } 59 | 60 | _transformModuleDeclarations () { 61 | // TODO 62 | // const filePath = this.destinationPath(this.libDirectory, 'declarations.d.ts'); 63 | // const ast = j(this.fs.read(filePath).toString()); 64 | // const moduleDeclarations = ast.find(j.TSModuleDeclaration); 65 | } 66 | 67 | _getConfiguration () { 68 | const sqlPackages = { 69 | mysql: 'mysql2', 70 | mssql: 'mssql', 71 | postgres: 'pg', 72 | sqlite: 'sqlite3' 73 | // oracle: 'oracle' 74 | }; 75 | 76 | const { connectionString, database, adapter } = this.props; 77 | let parsed = {}; 78 | 79 | if (adapter === 'objection') { 80 | this.dependencies.push('knex'); 81 | } else if (adapter === 'cassandra') { 82 | this.dependencies.push('express-cassandra'); 83 | this.dependencies.push('cassanknex'); 84 | } else if (adapter === 'sequelize') { 85 | if (this.srcType === 'ts') { 86 | this.devDependencies.push('@types/validator'); 87 | } 88 | } else if (adapter === 'prisma') { 89 | this.dependencies.push('@prisma/client'); 90 | this.devDependencies.push('prisma'); 91 | } 92 | 93 | switch (database) { 94 | case 'nedb': 95 | this.dependencies.push('@seald-io/nedb'); 96 | return connectionString.substring(7, connectionString.length); 97 | 98 | case 'memory': 99 | return null; 100 | 101 | case 'mongodb': 102 | if (adapter !== 'prisma') { 103 | this.dependencies.push(adapter); 104 | this.dependencies.push('mongodb-core'); 105 | } 106 | return connectionString; 107 | 108 | case 'mysql': 109 | case 'mssql': 110 | // case oracle: 111 | case 'postgres': // eslint-disable-line no-fallthrough 112 | case 'sqlite': 113 | if (adapter !== 'prisma') { 114 | this.dependencies.push(adapter); 115 | } 116 | 117 | if (sqlPackages[database] && adapter !== 'prisma') { 118 | this.dependencies.push(sqlPackages[database]); 119 | } 120 | 121 | if (adapter === 'sequelize') { 122 | return connectionString; 123 | } 124 | 125 | if (adapter === 'prisma' && database === 'sqlite') { 126 | return `file:./${connectionString.substr(9)}`; 127 | } else if (adapter === 'prisma') { 128 | return connectionString; 129 | } 130 | 131 | return { 132 | client: sqlPackages[database], 133 | connection: (database === 'sqlite' && typeof connectionString === 'string') ? { 134 | filename: connectionString.substring(9, connectionString.length) 135 | } : connectionString 136 | }; 137 | 138 | case 'cassandra': 139 | if (typeof connectionString !== 'string') { 140 | return connectionString; 141 | } 142 | 143 | parsed = url.parse(connectionString); 144 | 145 | return { 146 | clientOptions: { 147 | contactPoints: [parsed.hostname], 148 | protocolOptions: { port: Number(parsed.port) || 9042 }, 149 | keyspace: parsed.path.substring(1, parsed.path.length), 150 | queryOptions: { consistency: 1 } 151 | }, 152 | ormOptions: { 153 | defaultReplicationStrategy: { 154 | class: 'SimpleStrategy', 155 | replication_factor: 1 156 | }, 157 | migration: 'alter', 158 | createKeyspace: true 159 | } 160 | }; 161 | 162 | case 'couchbase': 163 | this.dependencies.push(adapter); 164 | 165 | return { 166 | host: connectionString, 167 | options: { 168 | username: 'Administrator', 169 | password: 'supersecret' 170 | } 171 | }; 172 | 173 | default: 174 | throw new Error(`Invalid database '${database}'. Cannot assemble configuration.`); 175 | } 176 | } 177 | 178 | _writeConfiguration () { 179 | const { database } = this.props; 180 | const config = Object.assign({}, this.defaultConfig); 181 | const configuration = this._getConfiguration(); 182 | 183 | if (!config[database]) { 184 | config[database] = configuration; 185 | 186 | this.conflicter.force = true; 187 | this.fs.writeJSON( 188 | this.destinationPath(this.configDirectory, 'default.json'), 189 | config 190 | ); 191 | } 192 | } 193 | 194 | prompting () { 195 | this.checkPackage(); 196 | 197 | const databaseName = snakeCase(this.pkg.name); 198 | const { defaultConfig } = this; 199 | 200 | const getProps = answers => Object.assign({}, this.props, answers); 201 | const setProps = props => Object.assign(this.props, props); 202 | 203 | const prompts = [ 204 | { 205 | type: 'list', 206 | name: 'database', 207 | message: 'Which database are you connecting to?', 208 | choices (current) { 209 | const answers = getProps(current); 210 | const { adapter } = answers; 211 | 212 | const defaultChoices = [ 213 | { name: 'MySQL (MariaDB)', value: 'mysql' }, 214 | { name: 'PostgreSQL', value: 'postgres' }, 215 | { name: 'SQLite', value: 'sqlite' }, 216 | { name: 'SQL Server', value: 'mssql' }, 217 | { name: 'MongoDB', value: 'mongodb' }, 218 | { name: 'Couchbase', value: 'couchbase' } 219 | ]; 220 | 221 | if (adapter === 'prisma') { 222 | return defaultChoices.filter((db) => !['couchbase'].includes(db.value)); 223 | } 224 | 225 | return defaultChoices; 226 | }, 227 | when (current) { 228 | const answers = getProps(current); 229 | const { database, adapter } = answers; 230 | 231 | if (database) { 232 | return false; 233 | } 234 | 235 | switch (adapter) { 236 | case 'nedb': 237 | case 'memory': 238 | case 'mongodb': 239 | case 'cassandra': 240 | case 'couchbase': 241 | setProps({ database: adapter }); 242 | return false; 243 | case 'mongoose': 244 | setProps({ database: 'mongodb' }); 245 | return false; 246 | } 247 | 248 | return true; 249 | } 250 | }, 251 | { 252 | type: 'list', 253 | name: 'adapter', 254 | message: 'Which database adapter would you like to use?', 255 | default (current) { 256 | const answers = getProps(current); 257 | const { database } = answers; 258 | 259 | if (database === 'mongodb') { 260 | return 'mongoose'; 261 | } 262 | 263 | return 'sequelize'; 264 | }, 265 | choices (current) { 266 | const answers = getProps(current); 267 | const { database } = answers; 268 | const mongoOptions = [ 269 | { name: 'MongoDB Native', value: 'mongodb' }, 270 | { name: 'Mongoose', value: 'mongoose' } 271 | ]; 272 | const sqlOptions = [ 273 | { name: 'Sequelize', value: 'sequelize' }, 274 | { name: 'KnexJS', value: 'knex' }, 275 | { name: 'Objection', value: 'objection' }, 276 | { name: 'Prisma', value: 'prisma' } 277 | ]; 278 | const cassandraOptions = [ 279 | { name: 'Cassandra', value: 'cassandra' } 280 | ]; 281 | 282 | if (database === 'mongodb') { 283 | return mongoOptions; 284 | } 285 | 286 | if (database === 'cassandra') { 287 | return cassandraOptions; 288 | } 289 | 290 | // It's an SQL DB 291 | return sqlOptions; 292 | }, 293 | when (current) { 294 | const answers = getProps(current); 295 | const { database, adapter } = answers; 296 | 297 | if (adapter) { 298 | return false; 299 | } 300 | 301 | switch (database) { 302 | case 'nedb': 303 | case 'memory': 304 | case 'cassandra': 305 | case 'couchbase': 306 | return false; 307 | } 308 | 309 | return true; 310 | } 311 | }, 312 | { 313 | name: 'connectionString', 314 | message: 'What is the database connection string?', 315 | default (current) { 316 | const answers = getProps(current); 317 | const { database, adapter } = answers; 318 | const defaultConnectionStrings = { 319 | mongodb: `mongodb://localhost:27017/${databaseName}`, 320 | mysql: `mysql://root:@localhost:3306/${databaseName}`, 321 | nedb: 'nedb://../data', 322 | // oracle: `oracle://root:password@localhost:1521/${databaseName}`, 323 | postgres: `postgres://postgres:@localhost:5432/${databaseName}`, 324 | sqlite: `sqlite://${databaseName}.sqlite`, 325 | mssql: `mssql://root:password@localhost:1433/${databaseName}`, 326 | cassandra: `cassandra://localhost:9042/${databaseName}`, 327 | couchbase: 'couchbase://localhost' 328 | }; 329 | 330 | if (adapter === 'prisma' && database === 'sqlite') { 331 | return `file:./${databaseName}.db`; 332 | } 333 | 334 | return defaultConnectionStrings[database]; 335 | }, 336 | when (current) { 337 | const answers = getProps(current); 338 | const { database } = answers; 339 | const connection = defaultConfig[database]; 340 | 341 | if (connection) { 342 | if (connection.connection){ 343 | setProps({ connectionString:connection.connection }); 344 | } else { 345 | setProps({ connectionString:connection }); 346 | } 347 | return false; 348 | } 349 | 350 | return database !== 'memory'; 351 | } 352 | } 353 | ]; 354 | 355 | return this.prompt(prompts).then(props => { 356 | this.props = Object.assign(this.props, props); 357 | }); 358 | } 359 | 360 | writing () { 361 | const { adapter } = this.props; 362 | const context = Object.assign({}, this.props); 363 | 364 | let template; 365 | 366 | if (adapter && adapter !== 'nedb') { 367 | template = `${adapter}`; 368 | } 369 | 370 | if (template) { 371 | const templateExists = this.fs.exists(this.srcDestinationPath(this.libDirectory, adapter)); 372 | 373 | // If the file doesn't exist yet, add it to the app.js 374 | if (!templateExists) { 375 | const appjs = this.srcDestinationPath(this.libDirectory, 'app'); 376 | 377 | this.conflicter.force = true; 378 | 379 | if (this.isTypescript) { 380 | this.fs.write(appjs, this._transformCodeTs( 381 | this.fs.read(appjs).toString() 382 | )); 383 | this._transformModuleDeclarations(); 384 | } else { 385 | this.fs.write(appjs, this._transformCode( 386 | this.fs.read(appjs).toString() 387 | )); 388 | } 389 | } 390 | 391 | // Copy template only if connection generate is not composed 392 | // from the service generator 393 | if(!templateExists || (templateExists && !this.props.service)) { 394 | this.fs.copyTpl( 395 | this.srcTemplatePath(template), 396 | this.srcDestinationPath(this.libDirectory, adapter), 397 | context 398 | ); 399 | } 400 | } 401 | 402 | this._writeConfiguration(); 403 | 404 | this._packagerInstall(this.dependencies, { 405 | save: true 406 | }); 407 | 408 | this._packagerInstall(this.devDependencies, { 409 | saveDev: true 410 | }); 411 | } 412 | 413 | end () { 414 | const { database, adapter, connectionString } = this.props; 415 | 416 | if (adapter === 'prisma') { 417 | const providers = { 418 | mssql: 'sqlserver', 419 | postgres: 'postgresql', 420 | }; 421 | this.log(`Run 'npx prisma init --datasource-provider ${providers[database] || database} --url ${connectionString}' in your command line!`); 422 | } 423 | 424 | // NOTE (EK): If this is the first time we set this up 425 | // show this nice message. 426 | if (connectionString && !this.defaultConfig[database]) { 427 | this.log(); 428 | 429 | switch (database) { 430 | case 'mongodb': 431 | case 'mssql': 432 | case 'mysql': 433 | // case 'oracle': 434 | case 'postgres': // eslint-disable-line no-fallthrough 435 | case 'cassandra': 436 | case 'couchbase': 437 | this.log(`Make sure that your ${database} database is running, the username/role is correct, and "${connectionString}" is reachable and the database has been created.`); 438 | this.log('Your configuration can be found in the projects config/ folder.'); 439 | break; 440 | } 441 | } 442 | } 443 | }; 444 | --------------------------------------------------------------------------------