packages/generators/src/service/index.ts
import { dirname } from 'path'
import _ from 'lodash'
import { runGenerator, runGenerators, prompt } from '@featherscloud/pinion'
import { fileURLToPath } from 'url'
import chalk from 'chalk'
import {
checkPreconditions,
DATABASE_TYPES,
FeathersBaseContext,
fileExists,
getDatabaseAdapter,
initializeBaseContext
} from '../commons.js'
// Set __dirname in es module
const __dirname = dirname(fileURLToPath(import.meta.url))
export interface ServiceGeneratorContext extends FeathersBaseContext {
/**
* The chosen service name
*/
name: string
/**
* The path the service is registered on
*/
path: string
/**
* The list of subfolders this service is in
*/
folder: string[]
/**
* The `camelCase` service name starting with a lowercase letter
*/
camelName: string
/**
* The `CamelCase` service name starting with an uppercase letter
*/
upperName: string
/**
* The service class name combined as `CamelCaseService`
*/
className: string
/**
* A kebab-cased (filename friendly) version of the service name
*/
kebabName: string
/**
* The actual filename (the last element of the path)
*/
fileName: string
/**
* The kebab-cased name of the path. Will be used for e.g. database names
*/
kebabPath: string
/**
* Indicates how many file paths we should go up to import other things (e.g. `../../`)
*/
relative: string
/**
* The chosen service type
*/
type: 'knex' | 'mongodb' | 'custom'
/**
* Which schema definition format to use
*/
schema: 'typebox' | 'json' | false
/**
* Wether this service uses authentication
*/
authentication: boolean
/**
* Set to true if this service is for an authentication entity
*/
isEntityService?: boolean
/**
* The authentication strategies (if it is an entity service)
*/
authStrategies: string[]
}
/**
* Parameters the generator is called with
*/
export type ServiceGeneratorArguments = FeathersBaseContext &
Partial<
Pick<ServiceGeneratorContext, 'name' | 'path' | 'type' | 'authentication' | 'isEntityService' | 'schema'>
>
export const generate = (ctx: ServiceGeneratorArguments) =>
Promise.resolve(ctx)
.then(initializeBaseContext())
.then(checkPreconditions())
.then(
prompt(({ name, path, type, schema, authentication, isEntityService, feathers, lib, language }) => {
const sqlDisabled = DATABASE_TYPES.every(
(name) => name === 'mongodb' || name === 'other' || !fileExists(lib, `${name}.${language}`)
)
const mongodbDisabled = !fileExists(lib, `mongodb.${language}`)
return [
{
name: 'name',
type: 'input',
when: !name,
message: 'What is the name of your service?',
validate: (input: any) => {
if (!input || input === 'authentication') {
return 'Invalid service name'
}
return true
}
},
{
name: 'path',
type: 'input',
when: !path,
message: 'Which path should the service be registered on?',
default: (answers: ServiceGeneratorArguments) => `${_.kebabCase(answers.name)}`,
validate: (input: any) => {
if (!input || input === 'authentication') {
return 'Invalid service path'
}
return true
}
},
{
name: 'authentication',
type: 'confirm',
when: authentication === undefined && !isEntityService,
message: 'Does this service require authentication?'
},
{
name: 'type',
type: 'list',
when: !type,
message: 'What database is the service using?',
default: getDatabaseAdapter(feathers?.database),
choices: [
{
value: 'knex',
name: `SQL${sqlDisabled ? chalk.gray(' (connection not available)') : ''}`,
disabled: sqlDisabled
},
{
value: 'mongodb',
name: `MongoDB${mongodbDisabled ? chalk.gray(' (connection not available)') : ''}`,
disabled: mongodbDisabled
},
{
value: 'custom',
name: 'A custom service'
}
]
},
{
name: 'schema',
type: 'list',
when: schema === undefined,
message: 'Which schema definition format do you want to use?',
suffix: chalk.grey(' Schemas allow to type, validate, secure and populate data'),
default: feathers?.schema,
choices: (answers: ServiceGeneratorContext) => [
{
value: 'typebox',
name: `TypeBox ${chalk.gray(' (recommended)')}`
},
{
value: 'json',
name: 'JSON schema'
},
{
value: false,
name: `No schema${
answers.type !== 'custom' ? chalk.gray(' (not recommended with a database)') : ''
}`
}
]
}
]
})
)
.then(async (ctx): Promise<ServiceGeneratorContext> => {
const { name, path, type, authStrategies = [] } = ctx as any as ServiceGeneratorContext
const kebabName = _.kebabCase(name)
const camelName = _.camelCase(name)
const upperName = _.upperFirst(camelName)
const className = `${upperName}Service`
const folder = path.split('/').filter((el) => el !== '')
const relative = ['', ...folder].map(() => '..').join('/')
const fileName = _.last(folder)
const kebabPath = _.kebabCase(path)
return {
name,
type,
path,
folder,
fileName,
upperName,
className,
kebabName,
camelName,
kebabPath,
relative,
authStrategies,
...ctx
} as ServiceGeneratorContext
})
.then(runGenerators<ServiceGeneratorContext>(__dirname, 'templates'))
.then(runGenerator<ServiceGeneratorContext>(__dirname, 'type', ({ type }) => `${type}.tpl.js`))