From 5b801b5017ddc3eaa95622b539f51d605916bc86 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Sun, 19 Jun 2022 21:48:18 -0700 Subject: [PATCH] feat(cli): Add typed client to a generated app (#2669) --- packages/authentication-oauth/src/index.ts | 18 ++++----- packages/cli/src/app/templates/client.tpl.ts | 27 ++++++++++++++ .../cli/src/app/templates/package.json.tpl.ts | 5 ++- .../src/app/templates/tsconfig.json.tpl.ts | 2 +- .../templates/declarations.tpl.ts | 2 +- .../templates/user.schema.tpl.ts | 6 ++- packages/cli/src/commons.ts | 1 + .../cli/src/service/templates/client.tpl.ts | 27 ++++++++++++++ packages/feathers/src/declarations.ts | 21 +++++++++++ packages/rest-client/src/index.ts | 37 ++++++------------- packages/rest-client/test/axios.test.ts | 2 +- packages/rest-client/test/fetch.test.ts | 2 +- packages/schema/src/query.ts | 2 +- packages/socketio-client/src/index.ts | 13 +++++-- packages/socketio-client/test/index.test.ts | 2 +- 15 files changed, 117 insertions(+), 50 deletions(-) create mode 100644 packages/cli/src/app/templates/client.tpl.ts create mode 100644 packages/cli/src/service/templates/client.tpl.ts diff --git a/packages/authentication-oauth/src/index.ts b/packages/authentication-oauth/src/index.ts index de15449b40..b72dd322be 100644 --- a/packages/authentication-oauth/src/index.ts +++ b/packages/authentication-oauth/src/index.ts @@ -42,18 +42,14 @@ export const setup = (options: OauthSetupSettings) => (app: Application) => { } } - const grant = defaultsDeep( - {}, - omit(oauth, ['redirect', 'origins']), - { - defaults: { - prefix: '/oauth', - origin: `${protocol}://${host}`, - transport: 'session', - response: ['tokens', 'raw', 'profile'] - } + const grant = defaultsDeep({}, omit(oauth, ['redirect', 'origins']), { + defaults: { + prefix: '/oauth', + origin: `${protocol}://${host}`, + transport: 'session', + response: ['tokens', 'raw', 'profile'] } - ) + }) const getUrl = (url: string) => { const { defaults } = grant diff --git a/packages/cli/src/app/templates/client.tpl.ts b/packages/cli/src/app/templates/client.tpl.ts new file mode 100644 index 0000000000..f99f042d3e --- /dev/null +++ b/packages/cli/src/app/templates/client.tpl.ts @@ -0,0 +1,27 @@ +import { generator, toFile } from '@feathershq/pinion' +import { renderSource } from '../../commons' +import { AppGeneratorContext } from '../index' + +const template = ({}: AppGeneratorContext) => + `import { feathers, type Service, type TransportConnection } from '@feathersjs/feathers' + +// A mapping of client side services +export interface ServiceTypes { +} + +export const createClient = (connection: TransportConnection) => { + const client = feathers() + + client.configure(connection) + + return client +} +` + +export const generate = async (ctx: AppGeneratorContext) => + generator(ctx).then( + renderSource( + template, + toFile(({ lib }) => lib, 'client') + ) + ) diff --git a/packages/cli/src/app/templates/package.json.tpl.ts b/packages/cli/src/app/templates/package.json.tpl.ts index 53effb8dde..0708f85fc2 100644 --- a/packages/cli/src/app/templates/package.json.tpl.ts +++ b/packages/cli/src/app/templates/package.json.tpl.ts @@ -13,8 +13,8 @@ const jsPackageJson = (lib: string) => ({ const tsPackageJson = (lib: string) => ({ scripts: { dev: `nodemon -x ts-node ${lib}/index.ts`, - compile: 'shx rm -rf lib/ && tsc', - start: 'npm run compile && node lib/', + compile: 'shx rm -rf dist/ && tsc', + start: 'npm run compile && node dist/', test: 'mocha test/ --require ts-node/register --recursive --extension .ts --exit' } }) @@ -54,6 +54,7 @@ const packageJson = ({ test }, main: `${lib}/`, + browser: language === 'ts' ? 'dist/client' : `${lib}/client`, ...(language === 'ts' ? tsPackageJson(lib) : jsPackageJson(lib)) }) diff --git a/packages/cli/src/app/templates/tsconfig.json.tpl.ts b/packages/cli/src/app/templates/tsconfig.json.tpl.ts index cd7c83e03b..f4c43be5b4 100644 --- a/packages/cli/src/app/templates/tsconfig.json.tpl.ts +++ b/packages/cli/src/app/templates/tsconfig.json.tpl.ts @@ -13,7 +13,7 @@ export const generate = (ctx: AppGeneratorContext) => compilerOptions: { target: 'es2020', module: 'commonjs', - outDir: './lib', + outDir: './dist', rootDir: `./${lib}`, strict: true, esModuleInterop: true diff --git a/packages/cli/src/authentication/templates/declarations.tpl.ts b/packages/cli/src/authentication/templates/declarations.tpl.ts index aaaec6e17b..8ac0316ca3 100644 --- a/packages/cli/src/authentication/templates/declarations.tpl.ts +++ b/packages/cli/src/authentication/templates/declarations.tpl.ts @@ -2,7 +2,7 @@ import { generator, inject, before, toFile, when, append } from '@feathershq/pin import { AuthenticationGeneratorContext } from '../index' const importTemplate = ({ upperName, schemaPath }: AuthenticationGeneratorContext) => - `import { ${upperName}Result } from './schemas/${schemaPath}' + `import { ${upperName}Result } from './${schemaPath}' ` const paramsTemplate = ({ diff --git a/packages/cli/src/authentication/templates/user.schema.tpl.ts b/packages/cli/src/authentication/templates/user.schema.tpl.ts index d2bd793691..a8082bb60e 100644 --- a/packages/cli/src/authentication/templates/user.schema.tpl.ts +++ b/packages/cli/src/authentication/templates/user.schema.tpl.ts @@ -51,7 +51,7 @@ export const ${camelName}ResultSchema = schema({ $id: '${upperName}Result', type: 'object', additionalProperties: false, - required: [ ...${camelName}DataSchema.required, '${type === 'mongodb' ? '_id' : 'id'}' ], + required: [ '${type === 'mongodb' ? '_id' : 'id'}' ], properties: { ...${camelName}DataSchema.properties, ${type === 'mongodb' ? '_id' : 'id'}: { @@ -62,6 +62,8 @@ export const ${camelName}ResultSchema = schema({ export type ${upperName}Result = Infer +// Queries shouldn't allow doing anything with the password +const { password, ...${camelName}QueryProperties } = ${camelName}ResultSchema.properties // Schema for allowed query properties export const ${camelName}QuerySchema = schema({ @@ -69,7 +71,7 @@ export const ${camelName}QuerySchema = schema({ type: 'object', additionalProperties: false, properties: { - ...querySyntax(${camelName}ResultSchema.properties) + ...querySyntax(${camelName}QueryProperties) } } as const) diff --git a/packages/cli/src/commons.ts b/packages/cli/src/commons.ts index bd01af9864..0d9aaf4605 100644 --- a/packages/cli/src/commons.ts +++ b/packages/cli/src/commons.ts @@ -69,6 +69,7 @@ export const initializeBaseContext = ...ctx, lib: ctx.pkg?.directories?.lib || 'src', test: ctx.pkg?.directories?.test || 'test', + language: ctx.pkg?.feathers?.language || 'ts', feathers: ctx.pkg?.feathers })) diff --git a/packages/cli/src/service/templates/client.tpl.ts b/packages/cli/src/service/templates/client.tpl.ts new file mode 100644 index 0000000000..f3a3fd4b18 --- /dev/null +++ b/packages/cli/src/service/templates/client.tpl.ts @@ -0,0 +1,27 @@ +import { generator, inject, toFile, when, after } from '@feathershq/pinion' +import { ServiceGeneratorContext } from '../index' + +const schemaImports = ({ upperName, schemaPath }: ServiceGeneratorContext) => `import type { + ${upperName}Data, + ${upperName}Result, + ${upperName}Query, +} from './${schemaPath}'` +const declarationTemplate = ({ path, upperName }: ServiceGeneratorContext) => + ` '${path}': Service<${upperName}Data, ${upperName}Result, Params<${upperName}Query>>` + +const toClientFile = toFile(({ lib, language }) => [lib, `client.${language}`]) + +export const generate = async (ctx: ServiceGeneratorContext) => + generator(ctx) + .then( + when( + (ctx) => ctx.language === 'ts', + inject(schemaImports, after("from '@feathersjs/feathers'"), toClientFile) + ) + ) + .then( + when( + (ctx) => ctx.language === 'ts', + inject(declarationTemplate, after('export interface ServiceTypes'), toClientFile) + ) + ) diff --git a/packages/feathers/src/declarations.ts b/packages/feathers/src/declarations.ts index a63416bac0..b03e613a25 100644 --- a/packages/feathers/src/declarations.ts +++ b/packages/feathers/src/declarations.ts @@ -6,6 +6,9 @@ type OptionalPick = Pick> export type { NextFunction } +/** + * The object returned from `.find` call by standard database adapters + */ export interface Paginated { total: number limit: number @@ -13,6 +16,9 @@ export interface Paginated { data: T[] } +/** + * Options that can be passed when registering a service via `app.use(name, service, options)` + */ export interface ServiceOptions { events?: string[] methods?: string[] @@ -89,6 +95,21 @@ export type CustomMethods = { [K in keyof T]: (data: T[K][0], params?: Params) => Promise } +/** + * An interface usually use by transport clients that represents a e.g. HTTP or websocket + * connection that can be configured on the application. + */ +export type TransportConnection = { + (app: Application): void + Service: any + service: ( + name: L + ) => keyof any extends keyof Services ? ServiceInterface : Services[L] +} + +/** + * The interface for a custom service method. Can e.g. be used to type client side services. + */ export type CustomMethod = (data: T, params?: P) => Promise export type ServiceMixin = (service: FeathersService, path: string, options: ServiceOptions) => void diff --git a/packages/rest-client/src/index.ts b/packages/rest-client/src/index.ts index 49e3166d25..d0afbaf7a5 100644 --- a/packages/rest-client/src/index.ts +++ b/packages/rest-client/src/index.ts @@ -1,4 +1,4 @@ -import { Application, defaultServiceMethods } from '@feathersjs/feathers' +import { Application, TransportConnection, defaultServiceMethods } from '@feathersjs/feathers' import { Base } from './base' import { AxiosClient } from './axios' @@ -13,34 +13,21 @@ const transports = { axios: AxiosClient } -interface HandlerResult extends Function { - /** - * initialize service - */ - (): void - - /** - * Transport Service - */ - Service: any - - /** - * default Service - */ - service: any -} - -export type Handler = (connection: any, options?: any, Service?: any) => HandlerResult +export type Handler = ( + connection: any, + options?: any, + Service?: any +) => TransportConnection -export interface Transport { - superagent: Handler - fetch: Handler - axios: Handler +export interface Transport { + superagent: Handler + fetch: Handler + axios: Handler } export type RestService> = Base -export default function restClient(base = '') { +export default function restClient(base = '') { const result: any = { Base } Object.keys(transports).forEach((key) => { @@ -81,7 +68,7 @@ export default function restClient(base = '') { } }) - return result as Transport + return result as Transport } if (typeof module !== 'undefined') { diff --git a/packages/rest-client/test/axios.test.ts b/packages/rest-client/test/axios.test.ts index 1dc6ba7828..84da787221 100644 --- a/packages/rest-client/test/axios.test.ts +++ b/packages/rest-client/test/axios.test.ts @@ -12,7 +12,7 @@ import { ServiceTypes } from './declarations' describe('Axios REST connector', function () { const url = 'http://localhost:8889' - const connection = rest(url).axios(axios) + const connection = rest(url).axios(axios) const app = feathers() .configure(connection) .use('todos', connection.service('todos'), { diff --git a/packages/rest-client/test/fetch.test.ts b/packages/rest-client/test/fetch.test.ts index bd4b380aa8..54ea3cec8e 100644 --- a/packages/rest-client/test/fetch.test.ts +++ b/packages/rest-client/test/fetch.test.ts @@ -11,7 +11,7 @@ import { ServiceTypes } from './declarations' describe('fetch REST connector', function () { const url = 'http://localhost:8889' - const connection = rest(url).fetch(fetch) + const connection = rest(url).fetch(fetch) const app = feathers() .configure(connection) .use('todos', connection.service('todos'), { diff --git a/packages/schema/src/query.ts b/packages/schema/src/query.ts index a8535fda8c..e786069ea5 100644 --- a/packages/schema/src/query.ts +++ b/packages/schema/src/query.ts @@ -63,7 +63,7 @@ export const queryProperties = (definit return result }, {} as { [K in keyof T]: PropertyQuery }) -export const querySyntax = (definition: T) => +export const querySyntax = (definition: T) => ({ $limit: { type: 'number', diff --git a/packages/socketio-client/src/index.ts b/packages/socketio-client/src/index.ts index 1b43ee744b..34c9102982 100644 --- a/packages/socketio-client/src/index.ts +++ b/packages/socketio-client/src/index.ts @@ -1,6 +1,11 @@ import { Service, SocketService } from '@feathersjs/transport-commons/client' import { Socket } from 'socket.io-client' -import { Application, defaultEventMap, defaultServiceMethods } from '@feathersjs/feathers' +import { + Application, + TransportConnection, + defaultEventMap, + defaultServiceMethods +} from '@feathersjs/feathers' export { SocketService } @@ -14,7 +19,7 @@ declare module '@feathersjs/feathers/lib/declarations' { } } -export default function socketioClient(connection: Socket, options?: any) { +export default function socketioClient(connection: Socket, options?: any) { if (!connection) { throw new Error('Socket.io connection needs to be provided') } @@ -31,7 +36,7 @@ export default function socketioClient(connection: Socket, options?: any) { return new Service(settings) as any } - const initialize = function (app: Application) { + const initialize = function (app: Application) { if (app.io !== undefined) { throw new Error('Only one default client provider can be configured') } @@ -50,7 +55,7 @@ export default function socketioClient(connection: Socket, options?: any) { initialize.Service = Service initialize.service = defaultService - return initialize + return initialize as TransportConnection } if (typeof module !== 'undefined') { diff --git a/packages/socketio-client/test/index.test.ts b/packages/socketio-client/test/index.test.ts index 239e61adb9..e7d45fbfa4 100644 --- a/packages/socketio-client/test/index.test.ts +++ b/packages/socketio-client/test/index.test.ts @@ -26,7 +26,7 @@ describe('@feathersjs/socketio-client', () => { server = await createServer().listen(9988) socket = io('http://localhost:9988') - const connection = socketio(socket) + const connection = socketio(socket) app.configure(connection) app.use('todos', connection.service('todos'), {