From 9e348d88381d5616eaf19bfea7e3b21467fca2a3 Mon Sep 17 00:00:00 2001 From: daffl Date: Thu, 9 Feb 2023 16:17:24 -0800 Subject: [PATCH 1/9] fix(generators): Final v5 generator tweaks --- packages/generators/src/service/templates/schema.typebox.tpl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/generators/src/service/templates/schema.typebox.tpl.ts b/packages/generators/src/service/templates/schema.typebox.tpl.ts index 66b6c38e26..f477245601 100644 --- a/packages/generators/src/service/templates/schema.typebox.tpl.ts +++ b/packages/generators/src/service/templates/schema.typebox.tpl.ts @@ -44,7 +44,7 @@ export const ${camelName}DataValidator = getValidator(${camelName}DataSchema, da export const ${camelName}DataResolver = resolve<${upperName}, HookContext>({}) // Schema for updating existing entries -export const ${camelName}PatchSchema = Type.Partial(${camelName}DataSchema, { +export const ${camelName}PatchSchema = Type.Partial(${camelName}Schema, { $id: '${upperName}Patch' }) export type ${upperName}Patch = Static From 01d977ae29512689f77ae046d2d36812c60526f5 Mon Sep 17 00:00:00 2001 From: daffl Date: Mon, 13 Feb 2023 12:04:56 -0800 Subject: [PATCH 2/9] Further minor generator change --- README.md | 2 +- packages/cli/README.md | 1 - packages/generators/README.md | 1 - packages/generators/src/service/type/custom.tpl.ts | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b3d2b4553b..4aaa84b45c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ $ cd my-new-app $ npm run dev ``` -To learn more about Feathers visit the website at [feathersjs.com](http://feathersjs.com) or jump right into [the Feathers guides](https://dove.feathersjs.com/guides/). +To learn more about Feathers visit the website at [feathersjs.com](http://feathersjs.com) or jump right into [the Feathers guides](https://feathersjs.com/guides/). # Documentation diff --git a/packages/cli/README.md b/packages/cli/README.md index f70c42640d..11ccaab417 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,7 +1,6 @@ # @feathersjs/cli [![CI](https://github.com/feathersjs/feathers/workflows/CI/badge.svg)](https://github.com/feathersjs/feathers/actions?query=workflow%3ACI) -[![Dependency Status](https://img.shields.io/david/feathersjs/feathers.svg?style=flat-square&path=packages/socketio)](https://david-dm.org/feathersjs/feathers?path=packages/cli) [![Download Status](https://img.shields.io/npm/dm/@feathersjs/cli.svg?style=flat-square)](https://www.npmjs.com/package/@feathersjs/cli) > The command line interface for creating Feathers applications diff --git a/packages/generators/README.md b/packages/generators/README.md index 5d7553f8ff..dd5d217193 100644 --- a/packages/generators/README.md +++ b/packages/generators/README.md @@ -1,7 +1,6 @@ # @feathersjs/generators [![CI](https://github.com/feathersjs/feathers/workflows/CI/badge.svg)](https://github.com/feathersjs/feathers/actions?query=workflow%3ACI) -[![Dependency Status](https://img.shields.io/david/feathersjs/feathers.svg?style=flat-square&path=packages/socketio)](https://david-dm.org/feathersjs/feathers?path=packages/generators) [![Download Status](https://img.shields.io/npm/dm/@feathersjs/generators.svg?style=flat-square)](https://www.npmjs.com/package/@feathersjs/cli) > Feathers core code generators used by the CLI powered by [Pinion](https://github.com/feathershq/pinion/) diff --git a/packages/generators/src/service/type/custom.tpl.ts b/packages/generators/src/service/type/custom.tpl.ts index e4800b6bfe..494734dd12 100644 --- a/packages/generators/src/service/type/custom.tpl.ts +++ b/packages/generators/src/service/type/custom.tpl.ts @@ -40,7 +40,7 @@ export interface ${upperName}Params extends Params<${upperName}Query> { } // This is a skeleton for a custom service class. Remove or add the methods you need here -export class ${className} +export class ${className} implements ServiceInterface<${upperName}, ${upperName}Data, ServiceParams, ${upperName}Patch> { constructor (public options: ${className}Options) { } From 5366da369c719a657b0621fcaf5c89613fb939d6 Mon Sep 17 00:00:00 2001 From: daffl Date: Thu, 16 Feb 2023 08:54:01 -0800 Subject: [PATCH 3/9] Refactor entity and default templates into single files --- .../generators/src/authentication/index.ts | 5 - .../templates/authentication.tpl.ts | 3 +- .../templates/client.test.tpl.ts | 3 +- .../src/authentication/templates/knex.tpl.ts | 56 -------- .../templates/schema.json.tpl.ts | 128 ------------------ .../templates/schema.typebox.tpl.ts | 114 ---------------- packages/generators/src/commons.ts | 19 +++ packages/generators/src/service/index.ts | 7 +- .../src/service/templates/schema.json.tpl.ts | 62 +++++++-- .../service/templates/schema.typebox.tpl.ts | 63 +++++++-- .../generators/src/service/type/knex.tpl.ts | 22 ++- .../src/service/type/mongodb.tpl.ts | 2 +- 12 files changed, 155 insertions(+), 329 deletions(-) delete mode 100644 packages/generators/src/authentication/templates/knex.tpl.ts delete mode 100644 packages/generators/src/authentication/templates/schema.json.tpl.ts delete mode 100644 packages/generators/src/authentication/templates/schema.typebox.tpl.ts diff --git a/packages/generators/src/authentication/index.ts b/packages/generators/src/authentication/index.ts index fb58c8480f..553e81c4e9 100644 --- a/packages/generators/src/authentication/index.ts +++ b/packages/generators/src/authentication/index.ts @@ -19,11 +19,6 @@ export interface AuthenticationGeneratorContext extends ServiceGeneratorContext export type AuthenticationGeneratorArguments = FeathersBaseContext & Partial> -export const localTemplate = (authStrategies: string[], content: string) => - authStrategies.includes('local') ? content : '' -export const oauthTemplate = (authStrategies: string[], content: string) => - authStrategies.filter((s) => s !== 'local').length > 0 ? content : '' - export const prompts = (ctx: AuthenticationGeneratorArguments) => [ { type: 'checkbox', diff --git a/packages/generators/src/authentication/templates/authentication.tpl.ts b/packages/generators/src/authentication/templates/authentication.tpl.ts index 2ff901b5d7..0eb27abe8c 100644 --- a/packages/generators/src/authentication/templates/authentication.tpl.ts +++ b/packages/generators/src/authentication/templates/authentication.tpl.ts @@ -1,6 +1,7 @@ import { generator, before, toFile } from '@feathershq/pinion' import { injectSource, renderSource } from '../../commons' -import { AuthenticationGeneratorContext, localTemplate, oauthTemplate } from '../index' +import { AuthenticationGeneratorContext } from '../index' +import { localTemplate, oauthTemplate } from '../../commons' const template = ({ authStrategies diff --git a/packages/generators/src/authentication/templates/client.test.tpl.ts b/packages/generators/src/authentication/templates/client.test.tpl.ts index 974a12cd8f..fc3d18038e 100644 --- a/packages/generators/src/authentication/templates/client.test.tpl.ts +++ b/packages/generators/src/authentication/templates/client.test.tpl.ts @@ -1,6 +1,7 @@ import { generator, toFile } from '@feathershq/pinion' import { renderSource } from '../../commons' -import { AuthenticationGeneratorContext, localTemplate } from '../index' +import { AuthenticationGeneratorContext } from '../index' +import { localTemplate } from '../../commons' const template = ({ authStrategies, diff --git a/packages/generators/src/authentication/templates/knex.tpl.ts b/packages/generators/src/authentication/templates/knex.tpl.ts deleted file mode 100644 index 048c414ae7..0000000000 --- a/packages/generators/src/authentication/templates/knex.tpl.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { generator, when, toFile } from '@feathershq/pinion' -import { getDatabaseAdapter, renderSource, yyyymmddhhmmss } from '../../commons' -import { AuthenticationGeneratorContext } from '../index' - -const migrationTemplate = ({ - kebabPath, - authStrategies -}: AuthenticationGeneratorContext) => /* ts */ `import type { Knex } from 'knex' - -export async function up(knex: Knex): Promise { - await knex.schema.alterTable('${kebabPath}', function (table) { - table.dropColumn('text')${authStrategies - .map((name) => - name === 'local' - ? ` - table.string('email').unique() - table.string('password')` - : ` - table.string('${name}Id')` - ) - .join('\n')} - }) -} - -export async function down(knex: Knex): Promise { - await knex.schema.alterTable('${kebabPath}', function (table) { - table.string('text')${authStrategies - .map((name) => - name === 'local' - ? ` - table.dropColumn('email') - table.dropColumn('password')` - : ` - table.dropColumn('${name}Id') - ` - ) - .join('\n')} - }) -} -` - -export const generate = (ctx: AuthenticationGeneratorContext) => - generator(ctx).then( - when( - (ctx) => getDatabaseAdapter(ctx.feathers?.database) === 'knex', - renderSource( - migrationTemplate, - toFile( - toFile( - 'migrations', - async () => `${yyyymmddhhmmss(1200)}_authentication` - ) - ) - ) - ) - ) diff --git a/packages/generators/src/authentication/templates/schema.json.tpl.ts b/packages/generators/src/authentication/templates/schema.json.tpl.ts deleted file mode 100644 index da7f2aa3c1..0000000000 --- a/packages/generators/src/authentication/templates/schema.json.tpl.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { generator, toFile, when } from '@feathershq/pinion' -import { fileExists, renderSource } from '../../commons' -import { AuthenticationGeneratorContext, localTemplate } from '../index' - -const template = ({ - cwd, - lib, - camelName, - upperName, - authStrategies, - type, - relative -}: AuthenticationGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html -import { resolve, querySyntax, getValidator } from '@feathersjs/schema'${ - type === 'mongodb' - ? ` -import { ObjectIdSchema } from '@feathersjs/schema'` - : '' -} -import type { FromSchema } from '@feathersjs/schema' -${localTemplate(authStrategies, `import { passwordHash } from '@feathersjs/authentication-local'`)} - -import type { HookContext } from '${relative}/declarations' -import { dataValidator, queryValidator } from '${relative}/${ - fileExists(cwd, lib, 'schemas') ? 'schemas/' : '' // This is for legacy backwards compatibility -}validators' - -// Main data model schema -export const ${camelName}Schema = { - $id: '${upperName}', - type: 'object', - additionalProperties: false, - required: [ '${type === 'mongodb' ? '_id' : 'id'}'${localTemplate(authStrategies, ", 'email'")} ], - properties: { - ${type === 'mongodb' ? `_id: ObjectIdSchema(),` : `id: { type: 'number' },`} - ${authStrategies - .map((name) => - name === 'local' - ? ` email: { type: 'string' }, - password: { type: 'string' }` - : ` ${name}Id: { type: 'string' }` - ) - .join(',\n')} - } -} as const -export type ${upperName} = FromSchema -export const ${camelName}Validator = getValidator(${camelName}Schema, dataValidator) -export const ${camelName}Resolver = resolve<${upperName}, HookContext>({}) - -export const ${camelName}ExternalResolver = resolve<${upperName}, HookContext>({ - ${localTemplate( - authStrategies, - `// The password should never be visible externally - password: async () => undefined` - )} -}) - -// Schema for creating new users -export const ${camelName}DataSchema = { - $id: '${upperName}Data', - type: 'object', - additionalProperties: false, - required: [ ], - properties: { - ...${camelName}Schema.properties - } -} as const -export type ${upperName}Data = FromSchema -export const ${camelName}DataValidator = getValidator(${camelName}DataSchema, dataValidator) -export const ${camelName}DataResolver = resolve<${upperName}Data, HookContext>({ - ${localTemplate(authStrategies, `password: passwordHash({ strategy: 'local' })`)} -}) - -// Schema for updating existing users -export const ${camelName}PatchSchema = { - $id: '${upperName}Patch', - type: 'object', - additionalProperties: false, - required: [], - properties: { - ...${camelName}Schema.properties - } -} as const -export type ${upperName}Patch = FromSchema -export const ${camelName}PatchValidator = getValidator(${camelName}PatchSchema, dataValidator) -export const ${camelName}PatchResolver = resolve<${upperName}Patch, HookContext>({ - ${localTemplate(authStrategies, `password: passwordHash({ strategy: 'local' })`)} -}) - -// Schema for allowed query properties -export const ${camelName}QuerySchema = { - $id: '${upperName}Query', - type: 'object', - additionalProperties: false, - properties: { - ...querySyntax(${camelName}Schema.properties) - } -} as const -export type ${upperName}Query = FromSchema -export const ${camelName}QueryValidator = getValidator(${camelName}QuerySchema, queryValidator) -export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>({ - // If there is a user (e.g. with authentication), they are only allowed to see their own data - ${type === 'mongodb' ? '_id' : 'id'}: async (value, user, context) => { - if (context.params.user) { - return context.params.user.${type === 'mongodb' ? '_id' : 'id'} - } - - return value - } -}) -` - -export const generate = (ctx: AuthenticationGeneratorContext) => - generator(ctx).then( - when( - ({ schema }) => schema === 'json', - renderSource( - template, - toFile(({ lib, folder, fileName }: AuthenticationGeneratorContext) => [ - lib, - 'services', - ...folder, - `${fileName}.schema` - ]), - { force: true } - ) - ) - ) diff --git a/packages/generators/src/authentication/templates/schema.typebox.tpl.ts b/packages/generators/src/authentication/templates/schema.typebox.tpl.ts deleted file mode 100644 index 909ec6ae5a..0000000000 --- a/packages/generators/src/authentication/templates/schema.typebox.tpl.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { generator, toFile, when } from '@feathershq/pinion' -import { fileExists, renderSource } from '../../commons' -import { AuthenticationGeneratorContext, localTemplate } from '../index' - -export const template = ({ - cwd, - lib, - camelName, - upperName, - authStrategies, - type, - relative -}: AuthenticationGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/service.schemas.html -import { resolve } from '@feathersjs/schema' -import { Type, getValidator, querySyntax } from '@feathersjs/typebox'${ - type === 'mongodb' - ? ` -import { ObjectIdSchema } from '@feathersjs/typebox'` - : '' -} -import type { Static } from '@feathersjs/typebox' -${localTemplate(authStrategies, `import { passwordHash } from '@feathersjs/authentication-local'`)} - -import type { HookContext } from '${relative}/declarations' -import { dataValidator, queryValidator } from '${relative}/${ - fileExists(cwd, lib, 'schemas') ? 'schemas/' : '' // This is for legacy backwards compatibility -}validators' - -// Main data model schema -export const ${camelName}Schema = Type.Object({ - ${type === 'mongodb' ? '_id: ObjectIdSchema()' : 'id: Type.Number()'}, - ${authStrategies - .map((name) => - name === 'local' - ? ` email: Type.String(), - password: Type.Optional(Type.String())` - : ` ${name}Id: Type.Optional(Type.String())` - ) - .join(',\n')} -},{ $id: '${upperName}', additionalProperties: false }) -export type ${upperName} = Static -export const ${camelName}Validator = getValidator(${camelName}Schema, dataValidator) -export const ${camelName}Resolver = resolve<${upperName}, HookContext>({}) - -export const ${camelName}ExternalResolver = resolve<${upperName}, HookContext>({ - ${localTemplate( - authStrategies, - `// The password should never be visible externally - password: async () => undefined` - )} -}) - -// Schema for creating new users -export const ${camelName}DataSchema = Type.Pick(${camelName}Schema, [ - ${authStrategies.map((name) => (name === 'local' ? `'email', 'password'` : `'${name}Id'`)).join(', ')} -], - { $id: '${upperName}Data', additionalProperties: false } -) -export type ${upperName}Data = Static -export const ${camelName}DataValidator = getValidator(${camelName}DataSchema, dataValidator) -export const ${camelName}DataResolver = resolve<${upperName}, HookContext>({ - ${localTemplate(authStrategies, `password: passwordHash({ strategy: 'local' })`)} -}) - -// Schema for updating existing users -export const ${camelName}PatchSchema = Type.Partial(${camelName}Schema, { - $id: '${upperName}Patch' -}) -export type ${upperName}Patch = Static -export const ${camelName}PatchValidator = getValidator(${camelName}PatchSchema, dataValidator) -export const ${camelName}PatchResolver = resolve<${upperName}, HookContext>({ - ${localTemplate(authStrategies, `password: passwordHash({ strategy: 'local' })`)} -}) - -// Schema for allowed query properties -export const ${camelName}QueryProperties = Type.Pick(${camelName}Schema, ['${ - type === 'mongodb' ? '_id' : 'id' -}', ${authStrategies.map((name) => (name === 'local' ? `'email'` : `'${name}Id'`)).join(', ')} -]) -export const ${camelName}QuerySchema = Type.Intersect([ - querySyntax(${camelName}QueryProperties), - // Add additional query properties here - Type.Object({}, { additionalProperties: false }) -], { additionalProperties: false }) -export type ${upperName}Query = Static -export const ${camelName}QueryValidator = getValidator(${camelName}QuerySchema, queryValidator) -export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>({ - // If there is a user (e.g. with authentication), they are only allowed to see their own data - ${type === 'mongodb' ? '_id' : 'id'}: async (value, user, context) => { - if (context.params.user) { - return context.params.user.${type === 'mongodb' ? '_id' : 'id'} - } - - return value - } -}) -` - -export const generate = (ctx: AuthenticationGeneratorContext) => - generator(ctx).then( - when( - ({ schema }) => schema === 'typebox', - renderSource( - template, - toFile(({ lib, folder, fileName }: AuthenticationGeneratorContext) => [ - lib, - 'services', - ...folder, - `${fileName}.schema` - ]), - { force: true } - ) - ) - ) diff --git a/packages/generators/src/commons.ts b/packages/generators/src/commons.ts index 345ebfa20c..188d3ed6d8 100644 --- a/packages/generators/src/commons.ts +++ b/packages/generators/src/commons.ts @@ -299,3 +299,22 @@ export const yyyymmddhhmmss = (offset = 0) => { now.getUTCSeconds().toString().padStart(2, '0') ) } + +/** + * Render a template if `local` authentication strategy has been selected + * @param authStrategies The list of selected authentication strategies + * @param content The content to render if `local` is selected + * @param alt The content to render if `local` is not selected + * @returns + */ +export const localTemplate = (authStrategies: string[], content: string, alt = '') => + authStrategies.includes('local') ? content : alt + +/** + * Render a template if an `oauth` authentication strategy has been selected + * @param authStrategies + * @param content + * @returns + */ +export const oauthTemplate = (authStrategies: string[], content: string) => + authStrategies.filter((s) => s !== 'local').length > 0 ? content : '' diff --git a/packages/generators/src/service/index.ts b/packages/generators/src/service/index.ts index 3011cd155c..cf2f190cb7 100644 --- a/packages/generators/src/service/index.ts +++ b/packages/generators/src/service/index.ts @@ -65,6 +65,10 @@ export interface ServiceGeneratorContext extends FeathersBaseContext { * Set to true if this service is for an authentication entity */ isEntityService?: boolean + /** + * The authentication strategies (if it is an entity service) + */ + authStrategies?: string[] } /** @@ -161,7 +165,7 @@ export const generate = (ctx: ServiceGeneratorArguments) => ) ) .then(async (ctx): Promise => { - const { name, path, type } = ctx + const { name, path, type, authStrategies = [] } = ctx const kebabName = _.kebabCase(name) const camelName = _.camelCase(name) const upperName = _.upperFirst(camelName) @@ -184,6 +188,7 @@ export const generate = (ctx: ServiceGeneratorArguments) => camelName, kebabPath, relative, + authStrategies, ...ctx } }) diff --git a/packages/generators/src/service/templates/schema.json.tpl.ts b/packages/generators/src/service/templates/schema.json.tpl.ts index 20951e9d06..531dfff7cc 100644 --- a/packages/generators/src/service/templates/schema.json.tpl.ts +++ b/packages/generators/src/service/templates/schema.json.tpl.ts @@ -1,11 +1,23 @@ import { generator, toFile, when } from '@feathershq/pinion' -import { fileExists, renderSource } from '../../commons' +import { fileExists, localTemplate, renderSource } from '../../commons' import { ServiceGeneratorContext } from '../index' +const authFieldsTemplate = (authStrategies: string[]) => + authStrategies + .map((name) => + name === 'local' + ? ` email: { type: 'string' }, + password: { type: 'string' }` + : ` ${name}Id: { type: 'string' }` + ) + .join(',\n') + const template = ({ camelName, upperName, relative, + authStrategies, + isEntityService, type, cwd, lib @@ -17,6 +29,7 @@ import { ObjectIdSchema } from '@feathersjs/schema'` : '' } import type { FromSchema } from '@feathersjs/schema' +${localTemplate(authStrategies, `import { passwordHash } from '@feathersjs/authentication-local'`)} import type { HookContext } from '${relative}/declarations' import { dataValidator, queryValidator } from '${relative}/${ @@ -28,33 +41,44 @@ export const ${camelName}Schema = { $id: '${upperName}', type: 'object', additionalProperties: false, - required: [ '${type === 'mongodb' ? '_id' : 'id'}', 'text' ], + required: [ '${type === 'mongodb' ? '_id' : 'id'}', ${localTemplate(authStrategies, `'email'`, `'text'`)} ], properties: { ${type === 'mongodb' ? `_id: ObjectIdSchema(),` : `id: { type: 'number' },`} - text: { type: 'string' } + ${ + isEntityService + ? authFieldsTemplate(authStrategies) + : ` + text: { type: 'string' }` + } } } as const export type ${upperName} = FromSchema export const ${camelName}Validator = getValidator(${camelName}Schema, dataValidator) export const ${camelName}Resolver = resolve<${upperName}, HookContext>({}) -export const ${camelName}ExternalResolver = resolve<${upperName}, HookContext>({}) +export const ${camelName}ExternalResolver = resolve<${upperName}, HookContext>({ + ${localTemplate( + authStrategies, + `// The password should never be visible externally + password: async () => undefined` + )} +}) // Schema for creating new data export const ${camelName}DataSchema = { $id: '${upperName}Data', type: 'object', additionalProperties: false, - required: [ 'text' ], + required: [ ${localTemplate(authStrategies, `'email'`, `'text'`)} ], properties: { - text: { - type: 'string' - } + ...${camelName}Schema.properties } } as const export type ${upperName}Data = FromSchema export const ${camelName}DataValidator = getValidator(${camelName}DataSchema, dataValidator) -export const ${camelName}DataResolver = resolve<${upperName}Data, HookContext>({}) +export const ${camelName}DataResolver = resolve<${upperName}Data, HookContext>({ + ${localTemplate(authStrategies, `password: passwordHash({ strategy: 'local' })`)} +}) // Schema for updating existing data export const ${camelName}PatchSchema = { @@ -68,7 +92,9 @@ export const ${camelName}PatchSchema = { } as const export type ${upperName}Patch = FromSchema export const ${camelName}PatchValidator = getValidator(${camelName}PatchSchema, dataValidator) -export const ${camelName}PatchResolver = resolve<${upperName}Patch, HookContext>({}) +export const ${camelName}PatchResolver = resolve<${upperName}Patch, HookContext>({ + ${localTemplate(authStrategies, `password: passwordHash({ strategy: 'local' })`)} +}) // Schema for allowed query properties export const ${camelName}QuerySchema = { @@ -81,7 +107,21 @@ export const ${camelName}QuerySchema = { } as const export type ${upperName}Query = FromSchema export const ${camelName}QueryValidator = getValidator(${camelName}QuerySchema, queryValidator) -export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>({}) +export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>({ + ${ + isEntityService + ? ` + // If there is a user (e.g. with authentication), they are only allowed to see their own data + ${type === 'mongodb' ? '_id' : 'id'}: async (value, user, context) => { + if (context.params.user) { + return context.params.user.${type === 'mongodb' ? '_id' : 'id'} + } + + return value + }` + : '' + } +}) ` export const generate = (ctx: ServiceGeneratorContext) => diff --git a/packages/generators/src/service/templates/schema.typebox.tpl.ts b/packages/generators/src/service/templates/schema.typebox.tpl.ts index f477245601..6397ca7592 100644 --- a/packages/generators/src/service/templates/schema.typebox.tpl.ts +++ b/packages/generators/src/service/templates/schema.typebox.tpl.ts @@ -1,11 +1,23 @@ import { generator, toFile, when } from '@feathershq/pinion' -import { fileExists, renderSource } from '../../commons' +import { fileExists, localTemplate, renderSource } from '../../commons' import { ServiceGeneratorContext } from '../index' +const authFieldsTemplate = (authStrategies: string[]) => + authStrategies + .map((name) => + name === 'local' + ? ` email: Type.String(), + password: Type.Optional(Type.String())` + : ` ${name}Id: Type.Optional(Type.String())` + ) + .join(',\n') + const template = ({ camelName, upperName, relative, + authStrategies, + isEntityService, type, cwd, lib @@ -18,6 +30,7 @@ import { ObjectIdSchema } from '@feathersjs/typebox'` : '' } import type { Static } from '@feathersjs/typebox' +${localTemplate(authStrategies, `import { passwordHash } from '@feathersjs/authentication-local'`)} import type { HookContext } from '${relative}/declarations' import { dataValidator, queryValidator } from '${relative}/${ @@ -27,21 +40,35 @@ import { dataValidator, queryValidator } from '${relative}/${ // Main data model schema export const ${camelName}Schema = Type.Object({ ${type === 'mongodb' ? '_id: ObjectIdSchema()' : 'id: Type.Number()'}, - text: Type.String() + ${isEntityService ? authFieldsTemplate(authStrategies) : `text: Type.String()`} }, { $id: '${upperName}', additionalProperties: false }) export type ${upperName} = Static export const ${camelName}Validator = getValidator(${camelName}Schema, dataValidator) export const ${camelName}Resolver = resolve<${upperName}, HookContext>({}) -export const ${camelName}ExternalResolver = resolve<${upperName}, HookContext>({}) +export const ${camelName}ExternalResolver = resolve<${upperName}, HookContext>({ + ${localTemplate( + authStrategies, + `// The password should never be visible externally + password: async () => undefined` + )} +}) // Schema for creating new entries -export const ${camelName}DataSchema = Type.Pick(${camelName}Schema, ['text'], { +export const ${camelName}DataSchema = Type.Pick(${camelName}Schema, [ + ${ + isEntityService + ? authStrategies.map((name) => (name === 'local' ? `'email', 'password'` : `'${name}Id'`)).join(', ') + : `'text'` + } +], { $id: '${upperName}Data' }) export type ${upperName}Data = Static export const ${camelName}DataValidator = getValidator(${camelName}DataSchema, dataValidator) -export const ${camelName}DataResolver = resolve<${upperName}, HookContext>({}) +export const ${camelName}DataResolver = resolve<${upperName}, HookContext>({ + ${localTemplate(authStrategies, `password: passwordHash({ strategy: 'local' })`)} +}) // Schema for updating existing entries export const ${camelName}PatchSchema = Type.Partial(${camelName}Schema, { @@ -49,11 +76,17 @@ export const ${camelName}PatchSchema = Type.Partial(${camelName}Schema, { }) export type ${upperName}Patch = Static export const ${camelName}PatchValidator = getValidator(${camelName}PatchSchema, dataValidator) -export const ${camelName}PatchResolver = resolve<${upperName}, HookContext>({}) +export const ${camelName}PatchResolver = resolve<${upperName}, HookContext>({ + ${localTemplate(authStrategies, `password: passwordHash({ strategy: 'local' })`)} +}) // Schema for allowed query properties export const ${camelName}QueryProperties = Type.Pick(${camelName}Schema, [ - '${type === 'mongodb' ? '_id' : 'id'}', 'text' + '${type === 'mongodb' ? '_id' : 'id'}', ${ + isEntityService + ? authStrategies.map((name) => (name === 'local' ? `'email'` : `'${name}Id'`)).join(', ') + : `'text'` +} ]) export const ${camelName}QuerySchema = Type.Intersect([ querySyntax(${camelName}QueryProperties), @@ -62,7 +95,21 @@ export const ${camelName}QuerySchema = Type.Intersect([ ], { additionalProperties: false }) export type ${upperName}Query = Static export const ${camelName}QueryValidator = getValidator(${camelName}QuerySchema, queryValidator) -export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>({}) +export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>({ + ${ + isEntityService + ? ` + // If there is a user (e.g. with authentication), they are only allowed to see their own data + ${type === 'mongodb' ? '_id' : 'id'}: async (value, user, context) => { + if (context.params.user) { + return context.params.user.${type === 'mongodb' ? '_id' : 'id'} + } + + return value + }` + : '' + } +}) ` export const generate = (ctx: ServiceGeneratorContext) => diff --git a/packages/generators/src/service/type/knex.tpl.ts b/packages/generators/src/service/type/knex.tpl.ts index 6a70145b3d..c52a0fafcc 100644 --- a/packages/generators/src/service/type/knex.tpl.ts +++ b/packages/generators/src/service/type/knex.tpl.ts @@ -3,14 +3,30 @@ import { renderSource, yyyymmddhhmmss } from '../../commons' import { ServiceGeneratorContext } from '../index' const migrationTemplate = ({ - kebabPath + kebabPath, + authStrategies, + isEntityService }: ServiceGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/knexfile.html import type { Knex } from 'knex' export async function up(knex: Knex): Promise { await knex.schema.createTable('${kebabPath}', table => { table.increments('id') - table.string('text') + ${ + isEntityService + ? authStrategies + .map((name) => + name === 'local' + ? ` + table.string('email').unique() + table.string('password')` + : ` + table.string('${name}Id')` + ) + .join('\n') + : ` + table.string('text')` + } }) } @@ -56,7 +72,7 @@ export interface ${upperName}Params extends KnexAdapterParams<${upperName}Query> // By default calls the standard Knex adapter service methods but can be customized with your own functionality. export class ${className} - extends KnexService<${upperName}, ${upperName}Data, ServiceParams, ${upperName}Patch> { + extends KnexService<${upperName}, ${upperName}Data, ${upperName}Params, ${upperName}Patch> { } export const getOptions = (app: Application): KnexAdapterOptions => { diff --git a/packages/generators/src/service/type/mongodb.tpl.ts b/packages/generators/src/service/type/mongodb.tpl.ts index 0645135138..a780fd2a6f 100644 --- a/packages/generators/src/service/type/mongodb.tpl.ts +++ b/packages/generators/src/service/type/mongodb.tpl.ts @@ -39,7 +39,7 @@ export interface ${upperName}Params extends MongoDBAdapterParams<${upperName}Que // By default calls the standard MongoDB adapter service methods but can be customized with your own functionality. export class ${className} - extends MongoDBService<${upperName}, ${upperName}Data, ServiceParams, ${upperName}Patch> { + extends MongoDBService<${upperName}, ${upperName}Data, ${upperName}Params, ${upperName}Patch> { } export const getOptions = (app: Application): MongoDBAdapterOptions => { From 6caacbe7d4ea0e9f0a45a32c25ce50ce87dc405b Mon Sep 17 00:00:00 2001 From: daffl Date: Thu, 16 Feb 2023 12:20:43 -0800 Subject: [PATCH 4/9] Make generated client optional --- .../cli/custom-environment-variables.md | 7 +++-- packages/create-feathers/bin/create-feathers | 2 +- packages/generators/src/app/index.ts | 11 ++++++++ .../src/app/templates/client.test.tpl.ts | 6 ++-- .../src/app/templates/client.tpl.ts | 11 +++++--- .../src/app/templates/configuration.tpl.ts | 5 +++- .../src/app/templates/package.json.tpl.ts | 11 ++++++-- .../templates/client.test.tpl.ts | 15 ++++++---- packages/generators/src/commons.ts | 2 +- packages/generators/src/service/index.ts | 2 +- .../src/service/templates/client.tpl.ts | 28 +++++++++---------- .../src/service/templates/service.tpl.ts | 13 +++++++-- .../src/service/templates/shared.tpl.ts | 23 ++++++++------- packages/generators/test/generators.test.ts | 1 + 14 files changed, 91 insertions(+), 46 deletions(-) diff --git a/docs/guides/cli/custom-environment-variables.md b/docs/guides/cli/custom-environment-variables.md index 97a77188c0..60419b4fa8 100644 --- a/docs/guides/cli/custom-environment-variables.md +++ b/docs/guides/cli/custom-environment-variables.md @@ -8,11 +8,14 @@ While `node-config` used for [application configuration](./default.json.md) reco "__name": "PORT", "__format": "number" }, - "host": "HOSTNAME" + "host": "HOSTNAME", + "authentication": { + "secret": "FEATHERS_SECRET" + } } ``` -This sets `app.get('port')` using the `PORT` environment variable (if it is available) parsing it as a number and `app.get('host')` from the `HOSTNAME` environment variable. +This sets `app.get('port')` using the `PORT` environment variable (if it is available) parsing it as a number and `app.get('host')` from the `HOSTNAME` environment variable and the authentication secret to the `FEATHERS_SECRET` environment variable.
diff --git a/packages/create-feathers/bin/create-feathers b/packages/create-feathers/bin/create-feathers index b9161e21ba..b95b40c186 100755 --- a/packages/create-feathers/bin/create-feathers +++ b/packages/create-feathers/bin/create-feathers @@ -39,7 +39,7 @@ ${chalk.grey('npm init feathers myapp')} ${chalk.green('Hooray')}! Your Feathers app is ready to go! 🚀 Go to the ${chalk.grey(name)} folder to get started. -To learn more visit ${chalk.grey('https://dove.feathersjs.com/guides')} +To learn more visit ${chalk.grey('https://feathersjs.com/guides')} `) } catch (error) { console.error(`${chalk.red('Error')}: ${error.message}`) diff --git a/packages/generators/src/app/index.ts b/packages/generators/src/app/index.ts index 7e419343f9..63bf65ad10 100644 --- a/packages/generators/src/app/index.ts +++ b/packages/generators/src/app/index.ts @@ -35,6 +35,10 @@ export interface AppGeneratorData extends FeathersAppInfo { * The source folder where files are put */ lib: string + /** + * Generate a client + */ + client: boolean } export type AppGeneratorContext = FeathersBaseContext & @@ -116,6 +120,13 @@ export const generate = (ctx: AppGeneratorArguments) => { value: 'pnpm', name: 'pnpm' } ] }, + { + name: 'client', + type: 'confirm', + when: ctx.client === undefined, + message: (answers) => `Generate ${answers.language === 'ts' ? 'end-to-end typed ' : ''}client?`, + suffix: chalk.grey(' Can be used in Node.js, React, Angular, Vue, React Native etc.') + }, { type: 'list', name: 'schema', diff --git a/packages/generators/src/app/templates/client.test.tpl.ts b/packages/generators/src/app/templates/client.test.tpl.ts index b2c7578275..aae82ae08e 100644 --- a/packages/generators/src/app/templates/client.test.tpl.ts +++ b/packages/generators/src/app/templates/client.test.tpl.ts @@ -1,4 +1,4 @@ -import { generator, toFile } from '@feathershq/pinion' +import { generator, toFile, when } from '@feathershq/pinion' import { renderSource } from '../../commons' import { AppGeneratorContext } from '../index' @@ -23,4 +23,6 @@ describe('client tests', () => { ` export const generate = (ctx: AppGeneratorContext) => - generator(ctx).then(renderSource(template, toFile('test', 'client.test'))) + generator(ctx).then( + when((ctx) => ctx.client, renderSource(template, toFile('test', 'client.test'))) + ) diff --git a/packages/generators/src/app/templates/client.tpl.ts b/packages/generators/src/app/templates/client.tpl.ts index 99ea0b6569..f70d3ef039 100644 --- a/packages/generators/src/app/templates/client.tpl.ts +++ b/packages/generators/src/app/templates/client.tpl.ts @@ -1,4 +1,4 @@ -import { generator, toFile } from '@feathershq/pinion' +import { generator, toFile, when } from '@feathershq/pinion' import { renderSource } from '../../commons' import { AppGeneratorContext } from '../index' @@ -43,8 +43,11 @@ export const createClient = ( export const generate = async (ctx: AppGeneratorContext) => generator(ctx).then( - renderSource( - template, - toFile(({ lib }) => lib, 'client') + when( + (ctx) => ctx.client, + renderSource( + template, + toFile(({ lib }) => lib, 'client') + ) ) ) diff --git a/packages/generators/src/app/templates/configuration.tpl.ts b/packages/generators/src/app/templates/configuration.tpl.ts index a19f826dae..22a5518d4c 100644 --- a/packages/generators/src/app/templates/configuration.tpl.ts +++ b/packages/generators/src/app/templates/configuration.tpl.ts @@ -18,7 +18,10 @@ const customEnvironment = { __name: 'PORT', __format: 'number' }, - host: 'HOSTNAME' + host: 'HOSTNAME', + authentication: { + secret: 'FEATHERS_SECRET' + } } const testConfig = { diff --git a/packages/generators/src/app/templates/package.json.tpl.ts b/packages/generators/src/app/templates/package.json.tpl.ts index d778bd2bf3..fae96737f3 100644 --- a/packages/generators/src/app/templates/package.json.tpl.ts +++ b/packages/generators/src/app/templates/package.json.tpl.ts @@ -29,6 +29,7 @@ const tsPackageJson = (lib: string) => ({ const packageJson = ({ name, description, + client, language, packager, database, @@ -62,8 +63,14 @@ const packageJson = ({ lib, test }, - files: ['lib/client.js', 'lib/**/*.d.ts', 'lib/**/*.shared.js'], - main: language === 'ts' ? 'lib/client' : `${lib}/client`, + ...(client + ? { + files: ['lib/client.js', 'lib/**/*.d.ts', 'lib/**/*.shared.js'], + main: language === 'ts' ? 'lib/client' : `${lib}/client` + } + : { + main: 'lib/index' + }), ...(language === 'ts' ? tsPackageJson(lib) : jsPackageJson(lib)) }) diff --git a/packages/generators/src/authentication/templates/client.test.tpl.ts b/packages/generators/src/authentication/templates/client.test.tpl.ts index fc3d18038e..0779f91e39 100644 --- a/packages/generators/src/authentication/templates/client.test.tpl.ts +++ b/packages/generators/src/authentication/templates/client.test.tpl.ts @@ -1,5 +1,5 @@ -import { generator, toFile } from '@feathershq/pinion' -import { renderSource } from '../../commons' +import { generator, toFile, when } from '@feathershq/pinion' +import { fileExists, renderSource } from '../../commons' import { AuthenticationGeneratorContext } from '../index' import { localTemplate } from '../../commons' @@ -67,9 +67,12 @@ describe('application client tests', () => { export const generate = (ctx: AuthenticationGeneratorContext) => generator(ctx).then( - renderSource( - template, - toFile(({ test }) => test, 'client.test'), - { force: true } + when( + ({ lib, language }) => fileExists(lib, `client.${language}`), + renderSource( + template, + toFile(({ test }) => test, 'client.test'), + { force: true } + ) ) ) diff --git a/packages/generators/src/commons.ts b/packages/generators/src/commons.ts index 188d3ed6d8..c455ab579b 100644 --- a/packages/generators/src/commons.ts +++ b/packages/generators/src/commons.ts @@ -45,7 +45,7 @@ export type FeathersAppInfo = { /** * The package manager used */ - packager: 'yarn' | 'npm' + packager: 'yarn' | 'npm' | 'pnpm' /** * A list of all chosen transports */ diff --git a/packages/generators/src/service/index.ts b/packages/generators/src/service/index.ts index cf2f190cb7..90d87fc1e6 100644 --- a/packages/generators/src/service/index.ts +++ b/packages/generators/src/service/index.ts @@ -68,7 +68,7 @@ export interface ServiceGeneratorContext extends FeathersBaseContext { /** * The authentication strategies (if it is an entity service) */ - authStrategies?: string[] + authStrategies: string[] } /** diff --git a/packages/generators/src/service/templates/client.tpl.ts b/packages/generators/src/service/templates/client.tpl.ts index 206da71557..38441bc484 100644 --- a/packages/generators/src/service/templates/client.tpl.ts +++ b/packages/generators/src/service/templates/client.tpl.ts @@ -1,5 +1,5 @@ -import { generator, toFile, after, before } from '@feathershq/pinion' -import { injectSource } from '../../commons' +import { generator, toFile, after, before, when } from '@feathershq/pinion' +import { fileExists, injectSource } from '../../commons' import { ServiceGeneratorContext } from '../index' const importTemplate = ({ upperName, folder, fileName, camelName }: ServiceGeneratorContext) => /* ts */ ` @@ -17,15 +17,15 @@ const registrationTemplate = ({ camelName }: ServiceGeneratorContext) => const toClientFile = toFile(({ lib }) => [lib, 'client']) -export const generate = async (ctx: ServiceGeneratorContext) => - generator(ctx) - .then(injectSource(registrationTemplate, before('return client'), toClientFile)) - .then( - injectSource( - importTemplate, - after(({ language }) => - language === 'ts' ? 'import type { AuthenticationClientOptions }' : 'import authenticationClient' - ), - toClientFile - ) - ) +export const generate = async (ctx: ServiceGeneratorContext) => generator(ctx) +when( + ({ lib, language }) => fileExists(lib, `client.${language}`), + injectSource(registrationTemplate, before('return client'), toClientFile), + injectSource( + importTemplate, + after(({ language }) => + language === 'ts' ? 'import type { AuthenticationClientOptions }' : 'import authenticationClient' + ), + toClientFile + ) +) diff --git a/packages/generators/src/service/templates/service.tpl.ts b/packages/generators/src/service/templates/service.tpl.ts index 08e0fe6604..1755f27328 100644 --- a/packages/generators/src/service/templates/service.tpl.ts +++ b/packages/generators/src/service/templates/service.tpl.ts @@ -1,11 +1,14 @@ import { generator, toFile, after, prepend } from '@feathershq/pinion' -import { injectSource, renderSource } from '../../commons' +import { fileExists, injectSource, renderSource } from '../../commons' import { ServiceGeneratorContext } from '../index' export const template = ({ camelName, authentication, isEntityService, + path, + lib, + language, className, relative, schema, @@ -33,7 +36,13 @@ import { import type { Application } from '${relative}/declarations' import { ${className}, getOptions } from './${fileName}.class' -import { ${camelName}Path, ${camelName}Methods } from './${fileName}.shared' +${ + fileExists(lib, `client.${language}`) + ? `import { ${camelName}Path, ${camelName}Methods } from './${fileName}.shared'` + : ` +export const ${camelName}Path = '${path}' +export const ${camelName}Methods = ['find', 'get', 'create', 'patch', 'remove'] as const` +} export * from './${fileName}.class' ${schema ? `export * from './${fileName}.schema'` : ''} diff --git a/packages/generators/src/service/templates/shared.tpl.ts b/packages/generators/src/service/templates/shared.tpl.ts index 981b80cdb6..e14a6298ac 100644 --- a/packages/generators/src/service/templates/shared.tpl.ts +++ b/packages/generators/src/service/templates/shared.tpl.ts @@ -1,5 +1,5 @@ -import { generator, toFile } from '@feathershq/pinion' -import { renderSource } from '../../commons' +import { generator, toFile, when } from '@feathershq/pinion' +import { fileExists, renderSource } from '../../commons' import { ServiceGeneratorContext } from '../index' const sharedTemplate = ({ @@ -49,13 +49,16 @@ declare module '${relative}/client' { export const generate = async (ctx: ServiceGeneratorContext) => generator(ctx).then( - renderSource( - sharedTemplate, - toFile(({ lib, folder, fileName }: ServiceGeneratorContext) => [ - lib, - 'services', - ...folder, - `${fileName}.shared` - ]) + when( + ({ lib, language }) => fileExists(lib, `client.${language}`), + renderSource( + sharedTemplate, + toFile(({ lib, folder, fileName }: ServiceGeneratorContext) => [ + lib, + 'services', + ...folder, + `${fileName}.shared` + ]) + ) ) ) diff --git a/packages/generators/test/generators.test.ts b/packages/generators/test/generators.test.ts index 02bf584904..6dfdf2814e 100644 --- a/packages/generators/test/generators.test.ts +++ b/packages/generators/test/generators.test.ts @@ -51,6 +51,7 @@ describe('@feathersjs/generators', () => { framework, language, dependencyVersions, + client: true, lib: 'src', description: 'A Feathers test app', packager: 'npm', From a901ad0823f3c24255d8912de7fcd6b1f7941959 Mon Sep 17 00:00:00 2001 From: daffl Date: Thu, 16 Feb 2023 13:28:08 -0800 Subject: [PATCH 5/9] Make authentication generation separate --- packages/generators/src/app/index.ts | 45 +------ .../generators/src/authentication/index.ts | 124 +++++++++--------- .../src/connection/templates/knex.tpl.ts | 6 + packages/generators/test/generators.test.ts | 23 +++- 4 files changed, 95 insertions(+), 103 deletions(-) diff --git a/packages/generators/src/app/index.ts b/packages/generators/src/app/index.ts index 63bf65ad10..103eeea5b4 100644 --- a/packages/generators/src/app/index.ts +++ b/packages/generators/src/app/index.ts @@ -1,17 +1,7 @@ import { sep } from 'path' import chalk from 'chalk' -import { - generator, - prompt, - runGenerators, - fromFile, - install, - copyFiles, - toFile, - when -} from '@feathershq/pinion' +import { generator, prompt, runGenerators, fromFile, install, copyFiles, toFile } from '@feathershq/pinion' import { FeathersBaseContext, FeathersAppInfo, initializeBaseContext, addVersions } from '../commons' -import { generate as authenticationGenerator, prompts as authenticationPrompts } from '../authentication' import { generate as connectionGenerator, prompts as connectionPrompts } from '../connection' export interface AppGeneratorData extends FeathersAppInfo { @@ -23,10 +13,6 @@ export interface AppGeneratorData extends FeathersAppInfo { * A short description of the app */ description: string - /** - * The selected user authentication strategies - */ - authStrategies: string[] /** * The database connection string */ @@ -125,25 +111,20 @@ export const generate = (ctx: AppGeneratorArguments) => type: 'confirm', when: ctx.client === undefined, message: (answers) => `Generate ${answers.language === 'ts' ? 'end-to-end typed ' : ''}client?`, - suffix: chalk.grey(' Can be used in Node.js, React, Angular, Vue, React Native etc.') + suffix: chalk.grey(' Can be used with React, Angular, Vue, React Native, etc.') }, { type: 'list', name: 'schema', when: !ctx.schema, message: 'What is your preferred schema (model) definition format?', + suffix: chalk.grey(' Schemas allow to type, validate, secure and populate data'), choices: [ { value: 'typebox', name: `TypeBox ${chalk.grey('(recommended)')}` }, { value: 'json', name: 'JSON schema' } ] }, - ...connectionPrompts(ctx), - ...authenticationPrompts({ - ...ctx, - service: 'user', - path: 'users', - entity: 'user' - }) + ...connectionPrompts(ctx) ]) ) .then(runGenerators(__dirname, 'templates')) @@ -157,24 +138,6 @@ export const generate = (ctx: AppGeneratorArguments) => dependencies } }) - .then( - when( - ({ authStrategies }) => authStrategies.length > 0, - async (ctx) => { - const { dependencies } = await authenticationGenerator({ - ...ctx, - service: 'user', - path: 'users', - entity: 'user' - }) - - return { - ...ctx, - dependencies - } - } - ) - ) .then( install( ({ transports, framework, dependencyVersions, dependencies, schema }) => { diff --git a/packages/generators/src/authentication/index.ts b/packages/generators/src/authentication/index.ts index 553e81c4e9..7cc258407d 100644 --- a/packages/generators/src/authentication/index.ts +++ b/packages/generators/src/authentication/index.ts @@ -17,72 +17,74 @@ export interface AuthenticationGeneratorContext extends ServiceGeneratorContext } export type AuthenticationGeneratorArguments = FeathersBaseContext & - Partial> - -export const prompts = (ctx: AuthenticationGeneratorArguments) => [ - { - type: 'checkbox', - name: 'authStrategies', - when: !ctx.authStrategies, - message: 'Which authentication methods do you want to use?', - suffix: chalk.grey(' Other methods and providers can be added at any time.'), - choices: [ - { - name: 'Email + Password', - value: 'local', - checked: true - }, - { - name: 'Google', - value: 'google' - }, - { - name: 'Facebook', - value: 'facebook' - }, - { - name: 'Twitter', - value: 'twitter' - }, - { - name: 'GitHub', - value: 'github' - }, - { - name: 'Auth0', - value: 'auth0' - } - ] - }, - { - name: 'service', - type: 'input', - when: !ctx.service, - message: 'What is your authentication service name?', - default: 'user' - }, - { - name: 'path', - type: 'input', - when: !ctx.path, - message: 'What path should the service be registered on?', - default: 'users' - }, - { - name: 'entity', - type: 'input', - when: !ctx.entity, - message: 'What is your authenticated entity name?', - suffix: chalk.grey(' Will be available in params (e.g. params.user)'), - default: 'user' - } -] + Partial> export const generate = (ctx: AuthenticationGeneratorArguments) => generator(ctx) .then(initializeBaseContext()) .then(checkPreconditions()) - .then(prompt(prompts)) + .then( + prompt( + (ctx: AuthenticationGeneratorArguments) => [ + { + type: 'checkbox', + name: 'authStrategies', + when: !ctx.authStrategies, + message: 'Which authentication methods do you want to use?', + suffix: chalk.grey(' Other methods and providers can be added at any time.'), + choices: [ + { + name: 'Email + Password', + value: 'local', + checked: true + }, + { + name: 'Google', + value: 'google' + }, + { + name: 'Facebook', + value: 'facebook' + }, + { + name: 'Twitter', + value: 'twitter' + }, + { + name: 'GitHub', + value: 'github' + }, + { + name: 'Auth0', + value: 'auth0' + } + ] + }, + { + name: 'service', + type: 'input', + when: !ctx.service, + message: 'What is your authentication service name?', + default: 'user' + }, + { + name: 'path', + type: 'input', + when: !ctx.path, + message: 'What path should the service be registered on?', + default: 'users' + }, + { + name: 'entity', + type: 'input', + when: !ctx.entity, + message: 'What is your authenticated entity name?', + suffix: chalk.grey(' Will be available in params (e.g. params.user)'), + default: 'user' + } + ] + ) + ) .then(async (ctx) => { const serviceContext = await serviceGenerator({ ...ctx, diff --git a/packages/generators/src/connection/templates/knex.tpl.ts b/packages/generators/src/connection/templates/knex.tpl.ts index ff5a8db648..5ce1e5a810 100644 --- a/packages/generators/src/connection/templates/knex.tpl.ts +++ b/packages/generators/src/connection/templates/knex.tpl.ts @@ -1,6 +1,8 @@ import { generator, toFile, before, mergeJSON } from '@feathershq/pinion' import { ConnectionGeneratorContext } from '../index' import { injectSource, renderSource } from '../../commons' +import { mkdir } from 'fs/promises' +import path from 'path' const template = ({ database @@ -65,3 +67,7 @@ export const generate = (ctx: ConnectionGeneratorContext) => ) .then(injectSource(importTemplate, before('import { services } from'), toAppFile)) .then(injectSource(configureTemplate, before('app.configure(services)'), toAppFile)) + .then(async (ctx) => { + await mkdir(path.join(ctx.cwd, 'migrations')) + return ctx + }) diff --git a/packages/generators/test/generators.test.ts b/packages/generators/test/generators.test.ts index 6dfdf2814e..e3d3e56664 100644 --- a/packages/generators/test/generators.test.ts +++ b/packages/generators/test/generators.test.ts @@ -13,8 +13,10 @@ import { combinate, dependencyVersions } from './utils' import { generate as generateApp } from '../lib/app' import { generate as generateConnection } from '../lib/connection' +import { generate as generateAuthentication } from '../lib/authentication' import { generate as generateService } from '../lib/service' import { listAllFiles } from '@feathershq/pinion/lib/utils' +import { AuthenticationGeneratorArguments } from '../src/authentication' const matrix = { language: ['js', 'ts'] as const, @@ -58,7 +60,6 @@ describe('@feathersjs/generators', () => { database: 'sqlite', connectionString: `${name}.sqlite`, transports: ['rest', 'websockets'], - authStrategies: ['local', 'github'], schema }, { cwd } @@ -78,6 +79,26 @@ describe('@feathersjs/generators', () => { assert.strictEqual(testResult, 0) }) + it('generates authentication with database and passes tests', async () => { + const authContext = await generateAuthentication( + getContext( + { + dependencyVersions, + authStrategies: ['local', 'github'], + entity: 'user', + service: 'user', + path: 'users', + schema + }, + { cwd } + ) + ) + const testResult = await context.pinion.exec('npm', ['test'], { cwd }) + + assert.ok(authContext) + assert.strictEqual(testResult, 0) + }) + it('generates a MongoDB connection and service and passes tests', async () => { const connectionContext = await generateConnection( getContext( From 48dd3447dcc70fde01da2fb028094674e25601ed Mon Sep 17 00:00:00 2001 From: daffl Date: Thu, 16 Feb 2023 14:39:23 -0800 Subject: [PATCH 6/9] Make channel setup optional --- packages/generators/src/app/index.ts | 2 +- packages/generators/src/app/templates/app.tpl.ts | 7 ++++--- .../generators/src/app/templates/channels.tpl.ts | 16 +++++++--------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/generators/src/app/index.ts b/packages/generators/src/app/index.ts index 103eeea5b4..fe2d38f28d 100644 --- a/packages/generators/src/app/index.ts +++ b/packages/generators/src/app/index.ts @@ -111,7 +111,7 @@ export const generate = (ctx: AppGeneratorArguments) => type: 'confirm', when: ctx.client === undefined, message: (answers) => `Generate ${answers.language === 'ts' ? 'end-to-end typed ' : ''}client?`, - suffix: chalk.grey(' Can be used with React, Angular, Vue, React Native, etc.') + suffix: chalk.grey(' Can be used with React, Angular, Vue, React Native, Node.js etc.') }, { type: 'list', diff --git a/packages/generators/src/app/templates/app.tpl.ts b/packages/generators/src/app/templates/app.tpl.ts index aee3cd608d..18a3af8ff2 100644 --- a/packages/generators/src/app/templates/app.tpl.ts +++ b/packages/generators/src/app/templates/app.tpl.ts @@ -16,7 +16,7 @@ import type { Application } from './declarations' import { configurationValidator } from './configuration' import { logError } from './hooks/log-error' import { services } from './services/index' -import { channels } from './channels' +${transports.includes('websockets') ? `import { channels } from './channels'` : ''} const app: Application = koa(feathers()) @@ -38,11 +38,12 @@ ${ cors: { origin: app.get('origins') } -}))` +})) +app.configure(channels)` : '' } app.configure(services) -app.configure(channels) + // Register hooks that run on all service methods app.hooks({ diff --git a/packages/generators/src/app/templates/channels.tpl.ts b/packages/generators/src/app/templates/channels.tpl.ts index fff49d2944..8cb710222b 100644 --- a/packages/generators/src/app/templates/channels.tpl.ts +++ b/packages/generators/src/app/templates/channels.tpl.ts @@ -1,4 +1,4 @@ -import { generator, toFile } from '@feathershq/pinion' +import { generator, toFile, when } from '@feathershq/pinion' import { renderSource } from '../../commons' import { AppGeneratorContext } from '../index' @@ -12,11 +12,6 @@ import type { Application, HookContext } from './declarations' import { logger } from './logger' export const channels = (app: Application) => { - if(typeof app.channel !== 'function') { - // If no real-time functionality has been configured just return - return - } - logger.warn('Publishing all events to all authenticated users. See \`channels.${language}\` and https://dove.feathersjs.com/api/channels.html for more information.') app.on('connection', (connection: RealTimeConnection) => { @@ -49,8 +44,11 @@ export const channels = (app: Application) => { export const generate = (ctx: AppGeneratorContext) => generator(ctx).then( - renderSource( - template, - toFile(({ lib }) => lib, 'channels') + when( + ({ transports }) => transports.includes('websockets'), + renderSource( + template, + toFile(({ lib }) => lib, 'channels') + ) ) ) From cbbff4c2ef45c5fa61ec4d16a71fa3e44bff83a6 Mon Sep 17 00:00:00 2001 From: daffl Date: Thu, 16 Feb 2023 15:22:55 -0800 Subject: [PATCH 7/9] Improve prompts --- packages/generators/src/app/index.ts | 3 +- .../generators/src/authentication/index.ts | 22 +--- packages/generators/src/commons.ts | 2 +- packages/generators/src/connection/index.ts | 108 ++++++++++-------- .../src/connection/templates/mongodb.tpl.ts | 20 ++-- packages/generators/test/generators.test.ts | 4 +- 6 files changed, 78 insertions(+), 81 deletions(-) diff --git a/packages/generators/src/app/index.ts b/packages/generators/src/app/index.ts index fe2d38f28d..ddd30f7467 100644 --- a/packages/generators/src/app/index.ts +++ b/packages/generators/src/app/index.ts @@ -121,7 +121,8 @@ export const generate = (ctx: AppGeneratorArguments) => suffix: chalk.grey(' Schemas allow to type, validate, secure and populate data'), choices: [ { value: 'typebox', name: `TypeBox ${chalk.grey('(recommended)')}` }, - { value: 'json', name: 'JSON schema' } + { value: 'json', name: 'JSON schema' }, + { value: false, name: `No schema ${chalk.grey('(less secure and not recommended)')}` } ] }, ...connectionPrompts(ctx) diff --git a/packages/generators/src/authentication/index.ts b/packages/generators/src/authentication/index.ts index 7cc258407d..72b8fc74e4 100644 --- a/packages/generators/src/authentication/index.ts +++ b/packages/generators/src/authentication/index.ts @@ -1,12 +1,6 @@ import chalk from 'chalk' import { generator, runGenerators, prompt, install } from '@feathershq/pinion' -import { - addVersions, - checkPreconditions, - FeathersBaseContext, - getDatabaseAdapter, - initializeBaseContext -} from '../commons' +import { addVersions, checkPreconditions, FeathersBaseContext, initializeBaseContext } from '../commons' import { generate as serviceGenerator, ServiceGeneratorContext } from '../service/index' export interface AuthenticationGeneratorContext extends ServiceGeneratorContext { @@ -17,7 +11,7 @@ export interface AuthenticationGeneratorContext extends ServiceGeneratorContext } export type AuthenticationGeneratorArguments = FeathersBaseContext & - Partial> + Partial> export const generate = (ctx: AuthenticationGeneratorArguments) => generator(ctx) @@ -73,14 +67,6 @@ export const generate = (ctx: AuthenticationGeneratorArguments) => when: !ctx.path, message: 'What path should the service be registered on?', default: 'users' - }, - { - name: 'entity', - type: 'input', - when: !ctx.entity, - message: 'What is your authenticated entity name?', - suffix: chalk.grey(' Will be available in params (e.g. params.user)'), - default: 'user' } ] ) @@ -89,12 +75,12 @@ export const generate = (ctx: AuthenticationGeneratorArguments) => const serviceContext = await serviceGenerator({ ...ctx, name: ctx.service, - isEntityService: true, - type: getDatabaseAdapter(ctx.feathers?.database) + isEntityService: true }) return { ...ctx, + entity: ctx.service, ...serviceContext } }) diff --git a/packages/generators/src/commons.ts b/packages/generators/src/commons.ts index c455ab579b..93044e3790 100644 --- a/packages/generators/src/commons.ts +++ b/packages/generators/src/commons.ts @@ -23,7 +23,7 @@ export type DependencyVersions = { [key: string]: string } /** * The database types supported by this generator */ -export type DatabaseType = 'mongodb' | 'mysql' | 'postgresql' | 'sqlite' | 'mssql' +export type DatabaseType = 'mongodb' | 'mysql' | 'postgresql' | 'sqlite' | 'mssql' | 'other' /** * Returns the name of the Feathers database adapter for a supported database type diff --git a/packages/generators/src/connection/index.ts b/packages/generators/src/connection/index.ts index 2c75039658..a990e7c263 100644 --- a/packages/generators/src/connection/index.ts +++ b/packages/generators/src/connection/index.ts @@ -1,4 +1,4 @@ -import { generator, runGenerator, prompt, install, mergeJSON, toFile } from '@feathershq/pinion' +import { generator, runGenerator, prompt, install, mergeJSON, toFile, when } from '@feathershq/pinion' import chalk from 'chalk' import { FeathersBaseContext, @@ -25,7 +25,8 @@ export const defaultConnectionString = (type: DatabaseType, name: string) => { mysql: `mysql://root:@localhost:3306/${name}`, postgresql: `postgres://postgres:@localhost:5432/${name}`, sqlite: `${name}.sqlite`, - mssql: `mssql://root:password@localhost:1433/${name}` + mssql: `mssql://root:password@localhost:1433/${name}`, + other: '' } return connectionStrings[type] @@ -37,21 +38,26 @@ export const prompts = ({ database, connectionString, pkg, name }: ConnectionGen type: 'list', when: !database, message: 'Which database are you connecting to?', - suffix: chalk.grey(' Other databases can be added at any time'), + suffix: chalk.grey(' Databases can be added at any time'), choices: [ { value: 'sqlite', name: 'SQLite' }, { value: 'mongodb', name: 'MongoDB' }, { value: 'postgresql', name: 'PostgreSQL' }, { value: 'mysql', name: 'MySQL/MariaDB' }, - { value: 'mssql', name: 'Microsoft SQL' } + { value: 'mssql', name: 'Microsoft SQL' }, + { + value: 'other', + name: `Another database ${chalk.grey('(used in custom services)')}` + } ] }, { name: 'connectionString', type: 'input', - when: !connectionString, + when: (answers: ConnectionGeneratorContext) => + !connectionString && database !== 'other' && answers.database !== 'other', message: 'Enter your database connection string', - default: (answers: { name?: string; database: DatabaseType }) => + default: (answers: ConnectionGeneratorContext) => defaultConnectionString(answers.database, answers.name || name || pkg.name) } ] @@ -64,7 +70,8 @@ export const DATABASE_CLIENTS = { mssql: 'mssql' } -export const getDatabaseClient = (database: DatabaseType) => DATABASE_CLIENTS[database] +export const getDatabaseClient = (database: DatabaseType) => + database === 'other' ? null : DATABASE_CLIENTS[database] export const generate = (ctx: ConnectionGeneratorArguments) => generator(ctx) @@ -72,52 +79,53 @@ export const generate = (ctx: ConnectionGeneratorArguments) => .then(checkPreconditions()) .then(prompt(prompts)) .then( - runGenerator( - __dirname, - 'templates', - ({ database }) => `${getDatabaseAdapter(database)}.tpl` - ) - ) - .then( - mergeJSON( - ({ connectionString, database }) => - getDatabaseAdapter(database) === 'knex' - ? { - [database]: { - client: getDatabaseClient(database), - connection: connectionString, - ...(database === 'sqlite' ? { useNullAsDefault: true } : {}) + when( + (ctx) => ctx.database !== 'other', + runGenerator( + __dirname, + 'templates', + ({ database }) => `${getDatabaseAdapter(database)}.tpl` + ), + mergeJSON( + ({ connectionString, database }) => + getDatabaseAdapter(database) === 'knex' + ? { + [database]: { + client: getDatabaseClient(database), + connection: connectionString, + ...(database === 'sqlite' ? { useNullAsDefault: true } : {}) + } } - } - : { - [database]: connectionString - }, - toFile('config', 'default.json') - ) - ) - .then((ctx: ConnectionGeneratorContext) => { - const dependencies: string[] = [] - const adapter = getDatabaseAdapter(ctx.database) - const dbClient = getDatabaseClient(ctx.database) + : { + [database]: connectionString + }, + toFile('config', 'default.json') + ), + async (ctx: ConnectionGeneratorContext) => { + const dependencies: string[] = [] + const adapter = getDatabaseAdapter(ctx.database) + const dbClient = getDatabaseClient(ctx.database) - dependencies.push(`@feathersjs/${adapter}`) + dependencies.push(`@feathersjs/${adapter}`) - if (adapter === 'knex') { - dependencies.push('knex') - } + if (adapter === 'knex') { + dependencies.push('knex') + } - dependencies.push(dbClient) + dependencies.push(dbClient) - if (ctx.dependencies) { - return { - ...ctx, - dependencies: [...ctx.dependencies, ...dependencies] - } - } + if (ctx.dependencies) { + return { + ...ctx, + dependencies: [...ctx.dependencies, ...dependencies] + } + } - return install( - addVersions(dependencies, ctx.dependencyVersions), - false, - ctx.feathers.packager - )(ctx) - }) + return install( + addVersions(dependencies, ctx.dependencyVersions), + false, + ctx.feathers.packager + )(ctx) + } + ) + ) diff --git a/packages/generators/src/connection/templates/mongodb.tpl.ts b/packages/generators/src/connection/templates/mongodb.tpl.ts index 21f745c2ed..64c3c38d7f 100644 --- a/packages/generators/src/connection/templates/mongodb.tpl.ts +++ b/packages/generators/src/connection/templates/mongodb.tpl.ts @@ -2,25 +2,26 @@ import { generator, toFile, before, prepend, append } from '@feathershq/pinion' import { ConnectionGeneratorContext } from '../index' import { injectSource, renderSource } from '../../commons' -const template = - ({}: ConnectionGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/databases.html +const template = ({ + database +}: ConnectionGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/databases.html import { MongoClient } from 'mongodb' import type { Db } from 'mongodb' import type { Application } from './declarations' declare module './declarations' { interface Configuration { - mongodbClient: Promise + ${database}Client: Promise } } -export const mongodb = (app: Application) => { - const connection = app.get('mongodb') as string +export const ${database} = (app: Application) => { + const connection = app.get('${database}') as string const database = new URL(connection).pathname.substring(1) const mongoClient = MongoClient.connect(connection) .then(client => client.db(database)) - app.set('mongodbClient', mongoClient) + app.set('${database}Client', mongoClient) } ` @@ -29,8 +30,9 @@ const keywordImport = `import { keywordObjectId } from '@feathersjs/mongodb'` const keywordTemplate = `dataValidator.addKeyword(keywordObjectId) queryValidator.addKeyword(keywordObjectId)` -const importTemplate = "import { mongodb } from './mongodb'" -const configureTemplate = 'app.configure(mongodb)' +const importTemplate = ({ database }: ConnectionGeneratorContext) => + `import { ${database} } from './${database}'` +const configureTemplate = ({ database }: ConnectionGeneratorContext) => `app.configure(${database})` const toAppFile = toFile(({ lib }) => [lib, 'app']) const toValidatorFile = toFile(({ lib }) => [lib, 'validators']) @@ -39,7 +41,7 @@ export const generate = (ctx: ConnectionGeneratorContext) => .then( renderSource( template, - toFile(({ lib }) => lib, 'mongodb') + toFile(({ lib, database }) => [lib, database]) ) ) .then(injectSource(importTemplate, before('import { services } from'), toAppFile)) diff --git a/packages/generators/test/generators.test.ts b/packages/generators/test/generators.test.ts index e3d3e56664..1d129ae1ca 100644 --- a/packages/generators/test/generators.test.ts +++ b/packages/generators/test/generators.test.ts @@ -79,15 +79,15 @@ describe('@feathersjs/generators', () => { assert.strictEqual(testResult, 0) }) - it('generates authentication with database and passes tests', async () => { + it('generates authentication with SQLite and passes tests', async () => { const authContext = await generateAuthentication( getContext( { dependencyVersions, authStrategies: ['local', 'github'], - entity: 'user', service: 'user', path: 'users', + type: 'knex', schema }, { cwd } From 9943f39621cb7c1e4b70dbef6dc8403f2fa602ed Mon Sep 17 00:00:00 2001 From: daffl Date: Thu, 16 Feb 2023 16:12:08 -0800 Subject: [PATCH 8/9] Disable selections that are not available --- packages/generators/src/commons.ts | 4 +- packages/generators/src/connection/index.ts | 2 +- packages/generators/src/service/index.ts | 158 +++++++++++--------- 3 files changed, 89 insertions(+), 75 deletions(-) diff --git a/packages/generators/src/commons.ts b/packages/generators/src/commons.ts index 93044e3790..5de52b2f67 100644 --- a/packages/generators/src/commons.ts +++ b/packages/generators/src/commons.ts @@ -20,10 +20,12 @@ export const { version } = JSON.parse(fs.readFileSync(join(__dirname, '..', 'pac export type DependencyVersions = { [key: string]: string } +export const DATABASE_TYPES = ['mongodb', 'mysql', 'postgresql', 'sqlite', 'mssql', 'other'] as const + /** * The database types supported by this generator */ -export type DatabaseType = 'mongodb' | 'mysql' | 'postgresql' | 'sqlite' | 'mssql' | 'other' +export type DatabaseType = (typeof DATABASE_TYPES)[number] /** * Returns the name of the Feathers database adapter for a supported database type diff --git a/packages/generators/src/connection/index.ts b/packages/generators/src/connection/index.ts index a990e7c263..3e3a3bd00c 100644 --- a/packages/generators/src/connection/index.ts +++ b/packages/generators/src/connection/index.ts @@ -47,7 +47,7 @@ export const prompts = ({ database, connectionString, pkg, name }: ConnectionGen { value: 'mssql', name: 'Microsoft SQL' }, { value: 'other', - name: `Another database ${chalk.grey('(used in custom services)')}` + name: `Another database ${chalk.grey('(not configured automatically, use with custom services)')}` } ] }, diff --git a/packages/generators/src/service/index.ts b/packages/generators/src/service/index.ts index 90d87fc1e6..67bd9a201d 100644 --- a/packages/generators/src/service/index.ts +++ b/packages/generators/src/service/index.ts @@ -3,10 +3,13 @@ import { generator, runGenerator, runGenerators, prompt } from '@feathershq/pini import { checkPreconditions, + DATABASE_TYPES, FeathersBaseContext, + fileExists, getDatabaseAdapter, initializeBaseContext } from '../commons' +import chalk from 'chalk' export interface ServiceGeneratorContext extends FeathersBaseContext { /** @@ -85,83 +88,92 @@ export const generate = (ctx: ServiceGeneratorArguments) => .then(checkPreconditions()) .then( prompt( - ({ name, path, type, schema, authentication, isEntityService, feathers }) => [ - { - name: 'name', - type: 'input', - when: !name, - message: 'What is the name of your service?', - validate: (input) => { - if (!input || input === 'authentication') { - return 'Invalid service name' - } + ({ 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 true - } - }, - { - name: 'path', - type: 'input', - when: !path, - message: 'Which path should the service be registered on?', - default: (answers: ServiceGeneratorArguments) => `${_.kebabCase(answers.name)}`, - validate: (input) => { - if (!input || input === 'authentication') { - return 'Invalid service path' - } + return [ + { + name: 'name', + type: 'input', + when: !name, + message: 'What is the name of your service?', + validate: (input) => { + if (!input || input === 'authentication') { + return 'Invalid service name' + } - return true - } - }, - { - name: 'authentication', - type: 'confirm', - when: authentication === undefined && !isEntityService, - message: 'Does this service require authentication?' - }, - { - name: 'type', - type: 'list', - when: !type, - message: 'What kind of service is it?', - default: getDatabaseAdapter(feathers?.database), - choices: [ - { - value: 'knex', - name: 'SQL' - }, - { - value: 'mongodb', - name: 'MongoDB' - }, - { - value: 'custom', - name: 'A custom service' + return true } - ] - }, - { - name: 'schema', - type: 'list', - when: schema === undefined, - message: 'Which schema definition format do you want to use?', - default: feathers?.schema, - choices: [ - { - value: 'typebox', - name: 'TypeBox' - }, - { - value: 'json', - name: 'JSON schema' - }, - { - value: false, - name: 'No schema' + }, + { + name: 'path', + type: 'input', + when: !path, + message: 'Which path should the service be registered on?', + default: (answers: ServiceGeneratorArguments) => `${_.kebabCase(answers.name)}`, + validate: (input) => { + 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 kind of service is it?', + default: getDatabaseAdapter(feathers?.database), + choices: [ + { + value: 'knex', + name: `SQL${sqlDisabled ? chalk.gray(' (connection not found)') : ''}`, + disabled: sqlDisabled + }, + { + value: 'mongodb', + name: `MongoDB${mongodbDisabled ? chalk.gray(' (connection not found)') : ''}`, + 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?', + default: feathers?.schema, + choices: [ + { + value: 'typebox', + name: 'TypeBox' + }, + { + value: 'json', + name: 'JSON schema' + }, + { + value: false, + name: 'No schema' + } + ] + } + ] + } ) ) .then(async (ctx): Promise => { From 00c67d08af59700cd637755aa0fff21f18499aaa Mon Sep 17 00:00:00 2001 From: daffl Date: Fri, 17 Feb 2023 08:46:06 -0800 Subject: [PATCH 9/9] Improve schmea choice --- packages/generators/src/app/index.ts | 6 ++++-- .../generators/src/app/templates/app.tpl.ts | 14 ++++++++------ .../src/app/templates/configuration.tpl.ts | 17 ++++++++++------- .../src/app/templates/declarations.tpl.ts | 9 +++++++-- packages/generators/src/commons.ts | 2 +- packages/generators/src/service/index.ts | 15 +++++++++------ 6 files changed, 39 insertions(+), 24 deletions(-) diff --git a/packages/generators/src/app/index.ts b/packages/generators/src/app/index.ts index ddd30f7467..58c1bf42c9 100644 --- a/packages/generators/src/app/index.ts +++ b/packages/generators/src/app/index.ts @@ -118,11 +118,13 @@ export const generate = (ctx: AppGeneratorArguments) => name: 'schema', when: !ctx.schema, message: 'What is your preferred schema (model) definition format?', - suffix: chalk.grey(' Schemas allow to type, validate, secure and populate data'), + suffix: chalk.grey( + ' Schemas allow to type, validate, secure and populate your data and configuration' + ), choices: [ { value: 'typebox', name: `TypeBox ${chalk.grey('(recommended)')}` }, { value: 'json', name: 'JSON schema' }, - { value: false, name: `No schema ${chalk.grey('(less secure and not recommended)')}` } + { value: false, name: `No schema ${chalk.grey('(not recommended)')}` } ] }, ...connectionPrompts(ctx) diff --git a/packages/generators/src/app/templates/app.tpl.ts b/packages/generators/src/app/templates/app.tpl.ts index 18a3af8ff2..13a6135df1 100644 --- a/packages/generators/src/app/templates/app.tpl.ts +++ b/packages/generators/src/app/templates/app.tpl.ts @@ -3,7 +3,8 @@ import { renderSource } from '../../commons' import { AppGeneratorContext } from '../index' const tsKoaApp = ({ - transports + transports, + schema }: AppGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/application.html import { feathers } from '@feathersjs/feathers' import configuration from '@feathersjs/configuration' @@ -12,8 +13,8 @@ import { } from '@feathersjs/koa' ${transports.includes('websockets') ? "import socketio from '@feathersjs/socketio'" : ''} +${schema !== false ? `import { configurationValidator } from './configuration'` : ''} import type { Application } from './declarations' -import { configurationValidator } from './configuration' import { logError } from './hooks/log-error' import { services } from './services/index' ${transports.includes('websockets') ? `import { channels } from './channels'` : ''} @@ -21,7 +22,7 @@ ${transports.includes('websockets') ? `import { channels } from './channels'` : const app: Application = koa(feathers()) // Load our app configuration (see config/ folder) -app.configure(configuration(configurationValidator)) +app.configure(configuration(${schema !== false ? 'configurationValidator' : ''})) // Set up Koa middleware app.use(cors()) @@ -64,7 +65,8 @@ export { app } ` const tsExpressApp = ({ - transports + transports, + schema }: AppGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/application.html import { feathers } from '@feathersjs/feathers' import express, { @@ -75,7 +77,7 @@ import configuration from '@feathersjs/configuration' ${transports.includes('websockets') ? "import socketio from '@feathersjs/socketio'" : ''} import type { Application } from './declarations' -import { configurationValidator } from './configuration' +${schema !== false ? `import { configurationValidator } from './configuration'` : ''} import { logger } from './logger' import { logError } from './hooks/log-error' import { services } from './services/index' @@ -84,7 +86,7 @@ import { channels } from './channels' const app: Application = express(feathers()) // Load app configuration -app.configure(configuration(configurationValidator)) +app.configure(configuration(${schema !== false ? 'configurationValidator' : ''})) app.use(cors()) app.use(json()) app.use(urlencoded({ extended: true })) diff --git a/packages/generators/src/app/templates/configuration.tpl.ts b/packages/generators/src/app/templates/configuration.tpl.ts index 22a5518d4c..cc2d61810e 100644 --- a/packages/generators/src/app/templates/configuration.tpl.ts +++ b/packages/generators/src/app/templates/configuration.tpl.ts @@ -1,4 +1,4 @@ -import { generator, toFile, writeJSON } from '@feathershq/pinion' +import { generator, toFile, when, writeJSON } from '@feathershq/pinion' import { renderSource } from '../../commons' import { AppGeneratorContext } from '../index' @@ -29,7 +29,7 @@ const testConfig = { } const configurationJsonTemplate = - ({}: AppGeneratorContext) => /* ts */ `import { defaultAppSettings, getValidator } from '@feathersjs/schema' + ({}: AppGeneratorContext) => `import { defaultAppSettings, getValidator } from '@feathersjs/schema' import type { FromSchema } from '@feathersjs/schema' import { dataValidator } from './validators' @@ -53,7 +53,7 @@ export type ApplicationConfiguration = FromSchema ` const configurationTypeboxTemplate = - ({}: AppGeneratorContext) => /* ts */ `import { Type, getValidator, defaultAppConfiguration } from '@feathersjs/typebox' + ({}: AppGeneratorContext) => `import { Type, getValidator, defaultAppConfiguration } from '@feathersjs/typebox' import type { Static } from '@feathersjs/typebox' import { dataValidator } from './validators' @@ -78,9 +78,12 @@ export const generate = (ctx: AppGeneratorContext) => .then(writeJSON(testConfig, toFile('config', 'test.json'))) .then(writeJSON(customEnvironment, toFile('config', 'custom-environment-variables.json'))) .then( - renderSource( - async (ctx) => - ctx.schema === 'typebox' ? configurationTypeboxTemplate(ctx) : configurationJsonTemplate(ctx), - toFile(({ lib }) => lib, 'configuration') + when( + (ctx) => ctx.schema !== false, + renderSource( + async (ctx) => + ctx.schema === 'typebox' ? configurationTypeboxTemplate(ctx) : configurationJsonTemplate(ctx), + toFile(({ lib }) => lib, 'configuration') + ) ) ) diff --git a/packages/generators/src/app/templates/declarations.tpl.ts b/packages/generators/src/app/templates/declarations.tpl.ts index 1e90fef7a9..afdef9060d 100644 --- a/packages/generators/src/app/templates/declarations.tpl.ts +++ b/packages/generators/src/app/templates/declarations.tpl.ts @@ -2,11 +2,16 @@ import { generator, toFile, when, renderTemplate } from '@feathershq/pinion' import { AppGeneratorContext } from '../index' const template = ({ - framework + framework, + schema }: AppGeneratorContext) => /* ts */ `// For more information about this file see https://dove.feathersjs.com/guides/cli/typescript.html import { HookContext as FeathersHookContext, NextFunction } from '@feathersjs/feathers' import { Application as FeathersApplication } from '@feathersjs/${framework}' -import { ApplicationConfiguration } from './configuration' +${ + schema === false + ? `type ApplicationConfiguration = any` + : `import { ApplicationConfiguration } from './configuration'` +} export { NextFunction } diff --git a/packages/generators/src/commons.ts b/packages/generators/src/commons.ts index 5de52b2f67..eb6dfd9916 100644 --- a/packages/generators/src/commons.ts +++ b/packages/generators/src/commons.ts @@ -59,7 +59,7 @@ export type FeathersAppInfo = { /** * The main schema definition format */ - schema: 'typebox' | 'json' + schema: 'typebox' | 'json' | false } export interface AppPackageJson extends PackageJson { diff --git a/packages/generators/src/service/index.ts b/packages/generators/src/service/index.ts index 67bd9a201d..22753daf02 100644 --- a/packages/generators/src/service/index.ts +++ b/packages/generators/src/service/index.ts @@ -132,17 +132,17 @@ export const generate = (ctx: ServiceGeneratorArguments) => name: 'type', type: 'list', when: !type, - message: 'What kind of service is it?', + message: 'What database is the service using?', default: getDatabaseAdapter(feathers?.database), choices: [ { value: 'knex', - name: `SQL${sqlDisabled ? chalk.gray(' (connection not found)') : ''}`, + name: `SQL${sqlDisabled ? chalk.gray(' (connection not available)') : ''}`, disabled: sqlDisabled }, { value: 'mongodb', - name: `MongoDB${mongodbDisabled ? chalk.gray(' (connection not found)') : ''}`, + name: `MongoDB${mongodbDisabled ? chalk.gray(' (connection not available)') : ''}`, disabled: mongodbDisabled }, { @@ -156,11 +156,12 @@ export const generate = (ctx: ServiceGeneratorArguments) => 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: [ + choices: (answers: ServiceGeneratorContext) => [ { value: 'typebox', - name: 'TypeBox' + name: `TypeBox ${chalk.gray(' (recommended)')}` }, { value: 'json', @@ -168,7 +169,9 @@ export const generate = (ctx: ServiceGeneratorArguments) => }, { value: false, - name: 'No schema' + name: `No schema${ + answers.type !== 'custom' ? chalk.gray(' (not recommended with a database)') : '' + }` } ] }