From eb2d15b8ff5be7cf9baadd25edc8d8ae2ec386e9 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Mon, 9 May 2022 16:23:18 +0700 Subject: [PATCH 01/64] Add function retrieve tsconfig from proejct to structure --- packages/structure/src/model/RWProject.ts | 27 +++++++ .../src/model/__tests__/model.test.ts | 73 ++++++++++++++----- 2 files changed, 81 insertions(+), 19 deletions(-) diff --git a/packages/structure/src/model/RWProject.ts b/packages/structure/src/model/RWProject.ts index b7e3b8f98451..8f49cd479a5c 100644 --- a/packages/structure/src/model/RWProject.ts +++ b/packages/structure/src/model/RWProject.ts @@ -2,6 +2,7 @@ import { join } from 'path' import { getDMMF } from '@prisma/sdk' import { DMMF } from '@prisma/generator-helper' +import * as ts from 'typescript' import { getPaths, processPagesDir } from '@redwoodjs/internal' @@ -128,6 +129,32 @@ export class RWProject extends BaseNode { return join(this.pathHelper.api.services, name, name + ext) } + @lazy() get getTsConfigs() { + const apiTsConfigPath = join(this.host.paths.api.base, 'tsconfig.json') + const webTsConfigPath = join(this.host.paths.web.base, 'tsconfig.json') + + + const apiTsConfig = this.host.existsSync(apiTsConfigPath) + ? ts.parseConfigFileTextToJson( + apiTsConfigPath, + this.host.readFileSync(apiTsConfigPath) + ) + : null + + const webTsConfig = this.host.existsSync(webTsConfigPath) + ? ts.parseConfigFileTextToJson( + webTsConfigPath, + this.host.readFileSync(webTsConfigPath) + ) + : null + + + return { + api: apiTsConfig?.config ?? null, + web: webTsConfig?.config ?? null, + } + } + // TODO: move to path helper @lazy() get defaultNotFoundPageFilePath() { const ext = this.isTypeScriptProject ? '.tsx' : '.js' // or jsx? diff --git a/packages/structure/src/model/__tests__/model.test.ts b/packages/structure/src/model/__tests__/model.test.ts index 8a24379a9d0e..03ed5ef8a5c5 100644 --- a/packages/structure/src/model/__tests__/model.test.ts +++ b/packages/structure/src/model/__tests__/model.test.ts @@ -23,29 +23,29 @@ describe('Redwood Project Model', () => { ]) ) for (const page of project.pages) { - page.basenameNoExt //? - page.route?.id //? + page.basenameNoExt + page.route?.id } - expect(project.sdls.map((s) => s.name)).toEqual(['currentUser', 'todos']) //? + expect(project.sdls.map((s) => s.name)).toEqual(['currentUser', 'todos']) for (const c of project.components) { - c.basenameNoExt //? + c.basenameNoExt } - project.components.length //? - project.components.map((c) => c.basenameNoExt) //? - project.functions.length //? - project.services.length //? - project.sdls.length //? + project.components.length + project.components.map((c) => c.basenameNoExt) + project.functions.length + project.services.length + project.sdls.length const ds = await project.collectDiagnostics() - ds.length //? - const uri = URL_file(projectRoot, 'api/src/graphql/todos.sdl.js') //? + ds.length + const uri = URL_file(projectRoot, 'api/src/graphql/todos.sdl.js') const node = await project.findNode(uri) expect(node).toBeDefined() expect(node.id).toEqual(uri) if (node) { const info = await node.collectIDEInfo() - info.length //? - info //? + info.length + info } }) @@ -93,16 +93,51 @@ describe('Cells', () => { }) }) +describe('Retrieves TSConfig settings', () => { + beforeAll(() => { + }) + afterAll(() => { + delete process.env.RWJS_CWD + }) + + it('Gets config for a TS Project', () => { + const TS_FIXTURE_PATH =getFixtureDir('test-project') + + process.env.RWJS_CWD = TS_FIXTURE_PATH + + const project = new RWProject({ projectRoot: TS_FIXTURE_PATH, host: new DefaultHost() }) //? + + expect(project.getTsConfigs.web).not.toBe(null) + expect(project.getTsConfigs.api).not.toBe(null) + + // Check some of the values + expect(project.getTsConfigs.web.compilerOptions.noEmit).toBe(true) + expect(project.getTsConfigs.api.compilerOptions.rootDirs).toEqual([ './src', '../.redwood/types/mirror/api/src' ]) + + }) + + it('Returns null for JS projects', () => { + const JS_FIXTURE_PATH =getFixtureDir('example-todo-main-with-errors') + + process.env.RWJS_CWD = JS_FIXTURE_PATH + + const project = new RWProject({ projectRoot: JS_FIXTURE_PATH, host: new DefaultHost() }) //? + + expect(project.getTsConfigs.web).toBe(null) + expect(project.getTsConfigs.api).toBe(null) + }) +}) + describe.skip('env vars', () => { it('Warns if env vars are not ok', async () => { const projectRoot = getFixtureDir('example-todo-main-with-errors') const project = new RWProject({ projectRoot, host: new DefaultHost() }) - project.envHelper.process_env_expressions.length //? + project.envHelper.process_env_expressions.length const env = project.envHelper - env.env //? - env.env_defaults //? - project.redwoodTOML.web_includeEnvironmentVariables //? - env.process_env_expressions //? + env.env + env.env_defaults + project.redwoodTOML.web_includeEnvironmentVariables + env.process_env_expressions }) }) @@ -135,7 +170,7 @@ describe('Redwood Route detection', () => { }) function getFixtureDir( - name: 'example-todo-main-with-errors' | 'example-todo-main' + name: 'example-todo-main-with-errors' | 'example-todo-main' | 'empty-project' | 'test-project' ) { return resolve(__dirname, `../../../../../__fixtures__/${name}`) } From df5844a44ad42a8a64691f570da7a3de8caf1c6e Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Tue, 10 May 2022 12:48:16 +0700 Subject: [PATCH 02/64] Move tsconfig detection to internal --- .../internal/src/__tests__/project.test.ts | 48 +++++++++++++++++++ packages/internal/src/project.ts | 31 ++++++++++++ packages/structure/src/model/RWProject.ts | 27 ----------- .../src/model/__tests__/model.test.ts | 35 -------------- 4 files changed, 79 insertions(+), 62 deletions(-) create mode 100644 packages/internal/src/__tests__/project.test.ts create mode 100644 packages/internal/src/project.ts diff --git a/packages/internal/src/__tests__/project.test.ts b/packages/internal/src/__tests__/project.test.ts new file mode 100644 index 000000000000..f1bd6189cf2d --- /dev/null +++ b/packages/internal/src/__tests__/project.test.ts @@ -0,0 +1,48 @@ +import path from 'path' + +import { getTsConfigs } from '../project' + +describe('Retrieves TSConfig settings', () => { + afterAll(() => { + delete process.env.RWJS_CWD + }) + + it('Gets config for a TS Project', () => { + const TS_FIXTURE_PATH = getFixtureDir('test-project') + + process.env.RWJS_CWD = TS_FIXTURE_PATH + + const tsConfiguration = getTsConfigs() + + expect(tsConfiguration.web).not.toBe(null) + expect(tsConfiguration.api).not.toBe(null) + + // Check some of the values + expect(tsConfiguration.web.compilerOptions.noEmit).toBe(true) + expect(tsConfiguration.api.compilerOptions.rootDirs).toEqual([ + './src', + '../.redwood/types/mirror/api/src', + ]) + }) + + it('Returns null for JS projects', () => { + const JS_FIXTURE_PATH = getFixtureDir('example-todo-main-with-errors') + + process.env.RWJS_CWD = JS_FIXTURE_PATH + + const tsConfiguration = getTsConfigs() + + expect(tsConfiguration.web).toBe(null) + expect(tsConfiguration.api).toBe(null) + }) +}) + +function getFixtureDir( + name: + | 'example-todo-main-with-errors' + | 'example-todo-main' + | 'empty-project' + | 'test-project' +) { + return path.resolve(__dirname, `../../../../__fixtures__/${name}`) +} diff --git a/packages/internal/src/project.ts b/packages/internal/src/project.ts new file mode 100644 index 000000000000..020c1a6f0621 --- /dev/null +++ b/packages/internal/src/project.ts @@ -0,0 +1,31 @@ +import fs from 'fs' +import path from 'path' + +import { parseConfigFileTextToJson } from 'typescript' + +import { getPaths } from './paths' + +export const getTsConfigs = () => { + const rwPaths = getPaths() + const apiTsConfigPath = path.join(rwPaths.api.base, 'tsconfig.json') + const webTsConfigPath = path.join(rwPaths.web.base, 'tsconfig.json') + + const apiTsConfig = fs.existsSync(apiTsConfigPath) + ? parseConfigFileTextToJson( + apiTsConfigPath, + fs.readFileSync(apiTsConfigPath, 'utf-8') + ) + : null + + const webTsConfig = fs.existsSync(webTsConfigPath) + ? parseConfigFileTextToJson( + webTsConfigPath, + fs.readFileSync(webTsConfigPath, 'utf-8') + ) + : null + + return { + api: apiTsConfig?.config ?? null, + web: webTsConfig?.config ?? null, + } +} diff --git a/packages/structure/src/model/RWProject.ts b/packages/structure/src/model/RWProject.ts index 8f49cd479a5c..b7e3b8f98451 100644 --- a/packages/structure/src/model/RWProject.ts +++ b/packages/structure/src/model/RWProject.ts @@ -2,7 +2,6 @@ import { join } from 'path' import { getDMMF } from '@prisma/sdk' import { DMMF } from '@prisma/generator-helper' -import * as ts from 'typescript' import { getPaths, processPagesDir } from '@redwoodjs/internal' @@ -129,32 +128,6 @@ export class RWProject extends BaseNode { return join(this.pathHelper.api.services, name, name + ext) } - @lazy() get getTsConfigs() { - const apiTsConfigPath = join(this.host.paths.api.base, 'tsconfig.json') - const webTsConfigPath = join(this.host.paths.web.base, 'tsconfig.json') - - - const apiTsConfig = this.host.existsSync(apiTsConfigPath) - ? ts.parseConfigFileTextToJson( - apiTsConfigPath, - this.host.readFileSync(apiTsConfigPath) - ) - : null - - const webTsConfig = this.host.existsSync(webTsConfigPath) - ? ts.parseConfigFileTextToJson( - webTsConfigPath, - this.host.readFileSync(webTsConfigPath) - ) - : null - - - return { - api: apiTsConfig?.config ?? null, - web: webTsConfig?.config ?? null, - } - } - // TODO: move to path helper @lazy() get defaultNotFoundPageFilePath() { const ext = this.isTypeScriptProject ? '.tsx' : '.js' // or jsx? diff --git a/packages/structure/src/model/__tests__/model.test.ts b/packages/structure/src/model/__tests__/model.test.ts index 03ed5ef8a5c5..383101ea9463 100644 --- a/packages/structure/src/model/__tests__/model.test.ts +++ b/packages/structure/src/model/__tests__/model.test.ts @@ -93,41 +93,6 @@ describe('Cells', () => { }) }) -describe('Retrieves TSConfig settings', () => { - beforeAll(() => { - }) - afterAll(() => { - delete process.env.RWJS_CWD - }) - - it('Gets config for a TS Project', () => { - const TS_FIXTURE_PATH =getFixtureDir('test-project') - - process.env.RWJS_CWD = TS_FIXTURE_PATH - - const project = new RWProject({ projectRoot: TS_FIXTURE_PATH, host: new DefaultHost() }) //? - - expect(project.getTsConfigs.web).not.toBe(null) - expect(project.getTsConfigs.api).not.toBe(null) - - // Check some of the values - expect(project.getTsConfigs.web.compilerOptions.noEmit).toBe(true) - expect(project.getTsConfigs.api.compilerOptions.rootDirs).toEqual([ './src', '../.redwood/types/mirror/api/src' ]) - - }) - - it('Returns null for JS projects', () => { - const JS_FIXTURE_PATH =getFixtureDir('example-todo-main-with-errors') - - process.env.RWJS_CWD = JS_FIXTURE_PATH - - const project = new RWProject({ projectRoot: JS_FIXTURE_PATH, host: new DefaultHost() }) //? - - expect(project.getTsConfigs.web).toBe(null) - expect(project.getTsConfigs.api).toBe(null) - }) -}) - describe.skip('env vars', () => { it('Warns if env vars are not ok', async () => { const projectRoot = getFixtureDir('example-todo-main-with-errors') From 6784575f20833dcc583f5a89348f8ba1a8f96bae Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Tue, 10 May 2022 17:36:38 +0700 Subject: [PATCH 03/64] Change resolver when strict mode is on --- .../internal/src/__tests__/resolverFn.test.ts | 84 +++++++++++++++++++ .../internal/src/generate/graphqlCodeGen.ts | 23 ++++- 2 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 packages/internal/src/__tests__/resolverFn.test.ts diff --git a/packages/internal/src/__tests__/resolverFn.test.ts b/packages/internal/src/__tests__/resolverFn.test.ts new file mode 100644 index 000000000000..651a394cc298 --- /dev/null +++ b/packages/internal/src/__tests__/resolverFn.test.ts @@ -0,0 +1,84 @@ +import path from 'path' + +import { getResolverFnType } from '../generate/graphqlCodeGen' + +const FIXTURE_PATH = path.resolve( + __dirname, + '../../../../__fixtures__/example-todo-main' +) + +// Pretend project is strict-mode +let mockedTSConfigs = { + api: null, + web: null, +} +jest.mock('../project', () => { + return { + getTsConfigs: () => { + return mockedTSConfigs + }, + } +}) + +beforeAll(() => { + process.env.RWJS_CWD = FIXTURE_PATH +}) + +afterAll(() => { + delete process.env.RWJS_CWD +}) + +afterEach(() => { + jest.restoreAllMocks() +}) + +describe('ResovlerFn types', () => { + it('Uses optional function args on JS projects', () => { + // Note args and obj are optional + expect(getResolverFnType()).toMatchInlineSnapshot(` + "( + args?: TArgs, + obj?: { root: TParent; context: TContext; info: GraphQLResolveInfo } + ) => Promise> | Partial;" + `) + }) + + it('Uses optional function args when strict mode is off', () => { + mockedTSConfigs = { + api: { + compilerOptions: { + strict: false, + }, + }, + web: null, + } + + // Note args and obj are optional + expect(getResolverFnType()).toMatchInlineSnapshot(` + "( + args?: TArgs, + obj?: { root: TParent; context: TContext; info: GraphQLResolveInfo } + ) => Promise> | Partial;" + `) + }) + + it('ResolverFn uses non-optional function args in strict mode', async () => { + // Prertend project is strict mode + mockedTSConfigs = { + api: { + compilerOptions: { + strict: true, + }, + }, + web: null, + } + + // Note args and obj are NOT optional + expect(getResolverFnType()).toMatchInlineSnapshot(` + "( + args: TArgs, + obj: { root: TParent; context: TContext; info: GraphQLResolveInfo } + ) => Promise> | Partial;" + `) + }) +}) diff --git a/packages/internal/src/generate/graphqlCodeGen.ts b/packages/internal/src/generate/graphqlCodeGen.ts index d69f7ce8f830..59ff02ab1b1c 100644 --- a/packages/internal/src/generate/graphqlCodeGen.ts +++ b/packages/internal/src/generate/graphqlCodeGen.ts @@ -18,6 +18,7 @@ import type { LoadTypedefsOptions } from '@graphql-tools/load' import { DocumentNode } from 'graphql' import { getPaths } from '../paths' +import { getTsConfigs } from '../project' export const generateTypeDefGraphQLApi = async () => { const filename = path.join(getPaths().api.types, 'graphql.d.ts') @@ -154,10 +155,7 @@ function getPluginConfig() { // Query/Mutation/etc omitOperationSuffix: true, showUnusedMappers: false, - customResolverFn: `( - args?: TArgs, - obj?: { root: TParent; context: TContext; info: GraphQLResolveInfo } - ) => Promise> | Partial;`, + customResolverFn: getResolverFnType(), mappers: prismaModels, contextType: `@redwoodjs/graphql-server/dist/functions/types#RedwoodGraphQLContext`, } @@ -165,6 +163,23 @@ function getPluginConfig() { return pluginConfig } +export const getResolverFnType = () => { + const tsConfig = getTsConfigs() + + if (tsConfig.api?.compilerOptions?.strict) { + // In strict mode, bring a world of pain to the tests + return `( + args: TArgs, + obj: { root: TParent; context: TContext; info: GraphQLResolveInfo } + ) => Promise> | Partial;` + } else { + return `( + args?: TArgs, + obj?: { root: TParent; context: TContext; info: GraphQLResolveInfo } + ) => Promise> | Partial;` + } +} + interface CombinedPluginConfig { name: string options: CodegenTypes.PluginConfig From 9bdb8669aa9f1ce3a63a0c85c7ec15c8d8e46d80 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Tue, 10 May 2022 17:44:52 +0700 Subject: [PATCH 04/64] Additional info on how to configure codegen --- docs/docs/project-configuration-dev-test-build.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/docs/project-configuration-dev-test-build.mdx b/docs/docs/project-configuration-dev-test-build.mdx index 28ef71678572..464ca1aa65d6 100644 --- a/docs/docs/project-configuration-dev-test-build.mdx +++ b/docs/docs/project-configuration-dev-test-build.mdx @@ -119,6 +119,8 @@ config: typeNames: change-case-all#upperCase ``` +Remember you can configure graphql-codegen in a number of different ways - codegen.yml, codegen.json or codegen.js - or even a key called codegen in your root package.json. Codegen uses [cosmiconfig](https://github.com/davidtheclark/cosmiconfig#cosmiconfig) under the hood, so take a peek at their docs for all the possible ways to configure. + For completeness, [here's the docs](https://www.graphql-code-generator.com/docs/config-reference/config-field) on configuring GraphQL Code Generator. Note that we currently only support the root level `config` option. ## Debugger configuration From 217b4e58e5244b9e985ac02a324ea7146da2f658 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Wed, 11 May 2022 15:24:37 +0700 Subject: [PATCH 05/64] Keep obj optional --- packages/internal/src/generate/graphqlCodeGen.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/internal/src/generate/graphqlCodeGen.ts b/packages/internal/src/generate/graphqlCodeGen.ts index 59ff02ab1b1c..d33b3a7bfedb 100644 --- a/packages/internal/src/generate/graphqlCodeGen.ts +++ b/packages/internal/src/generate/graphqlCodeGen.ts @@ -125,7 +125,7 @@ function getPluginConfig() { const localPrisma = require('@prisma/client') prismaModels = localPrisma.ModelName Object.keys(prismaModels).forEach((key) => { - prismaModels[key] = `.prisma/client#${key} as Prisma${key}` + prismaModels[key] = `@prisma/client#${key} as Prisma${key}` }) // This isn't really something you'd put in the GraphQL API, so // we can skip the model. @@ -170,7 +170,7 @@ export const getResolverFnType = () => { // In strict mode, bring a world of pain to the tests return `( args: TArgs, - obj: { root: TParent; context: TContext; info: GraphQLResolveInfo } + obj?: { root: TParent; context: TContext; info: GraphQLResolveInfo } ) => Promise> | Partial;` } else { return `( From 1da9eb22e828f19e45b6b888f378301a58b67f42 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Sun, 15 May 2022 14:45:14 +0700 Subject: [PATCH 06/64] Strict typing dbAuth handler | Export types --- .../api/src/functions/dbAuth/DbAuthHandler.ts | 53 ++++++++++++------- packages/eslint-config/shared.js | 1 + 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/packages/api/src/functions/dbAuth/DbAuthHandler.ts b/packages/api/src/functions/dbAuth/DbAuthHandler.ts index c1a13467dda1..6afd467ffdad 100644 --- a/packages/api/src/functions/dbAuth/DbAuthHandler.ts +++ b/packages/api/src/functions/dbAuth/DbAuthHandler.ts @@ -15,7 +15,7 @@ import { normalizeRequest } from '../../transforms' import * as DbAuthError from './errors' import { decryptSession, getSession } from './shared' -interface DbAuthHandlerOptions { +export interface DbAuthHandlerOptions> { /** * Provide prisma db client */ @@ -52,7 +52,7 @@ interface DbAuthHandlerOptions { * Object containing forgot password options */ forgotPassword: { - handler: (user: Record) => Promise + handler: (user: TUser) => Promise> | Partial errors?: { usernameNotFound?: string usernameRequired?: string @@ -70,7 +70,7 @@ interface DbAuthHandlerOptions { * in, containing at least an `id` field (whatever named field was provided * for `authFields.id`). For example: `return { id: user.id }` */ - handler: (user: Record) => Promise + handler: (user: TUser) => Promise> | Partial /** * Object containing error strings */ @@ -88,7 +88,7 @@ interface DbAuthHandlerOptions { * Object containing reset password options */ resetPassword: { - handler: (user: Record) => Promise + handler: (user: TUser) => Promise> | Partial allowReusedPassword: boolean errors?: { resetTokenExpired?: string @@ -109,7 +109,9 @@ interface DbAuthHandlerOptions { * were included in the object given to the `signUp()` function you got * from `useAuth()` */ - handler: (signupHandlerOptions: SignupHandlerOptions) => Promise + handler: ( + signupHandlerOptions: SignupHandlerArgs + ) => Promise> | Partial /** * Object containing error strings */ @@ -124,19 +126,30 @@ interface DbAuthHandlerOptions { */ cors?: CorsConfig } - -interface SignupHandlerOptions { +interface SignupHandlerArgs { username: string hashedPassword: string salt: string - userAttributes?: any + userAttributes?: Record } -interface SessionRecord { - id: string | number +/** + * To use in src/lib/auth#getCurrentUser + * + * Use this type to tell the getCurrentUser function what the type of session is + * @example + * import {User} from '@prisma/client' + * + * // key being used in dbAccessor in src/functions/auth.ts 👇 + * const getCurrentUser = async (session: DbAuthSession) + * + * + */ +export interface DbAuthSession { + id: TIdType } -type AuthMethodNames = +export type AuthMethodNames = | 'forgotPassword' | 'getToken' | 'login' @@ -149,19 +162,19 @@ type Params = { username?: string password?: string method: AuthMethodNames - [key: string]: unknown + [key: string]: any } -export class DbAuthHandler { +export class DbAuthHandler> { event: APIGatewayProxyEvent context: LambdaContext - options: DbAuthHandlerOptions + options: DbAuthHandlerOptions params: Params db: PrismaClient dbAccessor: any headerCsrfToken: string | undefined hasInvalidSession: boolean - session: SessionRecord | undefined + session: DbAuthSession | undefined sessionCsrfToken: string | undefined corsContext: CorsContext | undefined futureExpiresDate: string @@ -215,7 +228,7 @@ export class DbAuthHandler { constructor( event: APIGatewayProxyEvent, context: LambdaContext, - options: DbAuthHandlerOptions + options: DbAuthHandlerOptions ) { this.event = event this.context = context @@ -399,6 +412,7 @@ export class DbAuthHandler { async login() { const { username, password } = this.params const dbUser = await this._verifyUser(username, password) + const handlerUser = await this.options.login.handler(dbUser) if ( @@ -602,7 +616,7 @@ export class DbAuthHandler { // returns the Set-Cookie header to be returned in the request (effectively creates the session) _createSessionHeader( - data: SessionRecord, + data: DbAuthSession, csrfToken: string ): Record<'Set-Cookie', string> { const session = JSON.stringify(data) + ';' + csrfToken @@ -747,7 +761,7 @@ export class DbAuthHandler { // creates and returns a user, first checking that the username/password // values pass validation - async _createUser() { + async _createUser(): Promise | undefined> { const { username, password, ...userAttributes } = this.params if ( this._validateField('username', username) && @@ -775,6 +789,9 @@ export class DbAuthHandler { return newUser } + + // Handled internally when createUser is invoked + return undefined } // hashes a password using either the given `salt` argument, or creates a new diff --git a/packages/eslint-config/shared.js b/packages/eslint-config/shared.js index ca9afbef05d9..faf4ef29e12e 100644 --- a/packages/eslint-config/shared.js +++ b/packages/eslint-config/shared.js @@ -89,6 +89,7 @@ module.exports = { '@typescript-eslint/camelcase': 'off', 'no-use-before-define': 'off', '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/prefer-namespace-keyword': 'off', '@typescript-eslint/no-unused-vars': [ 'error', { varsIgnorePattern: '^_', argsIgnorePattern: '^_' }, From 79dd5f773f8364d5c8c61c1f6e4534a524e3970e Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Mon, 16 May 2022 15:59:19 +0700 Subject: [PATCH 07/64] Update resolverFn return types to handle when promises are passed in TResult --- .../__snapshots__/graphqlCodeGen.test.ts.snap | 54 ++++++++++--------- .../internal/src/__tests__/resolverFn.test.ts | 14 +++-- .../internal/src/generate/graphqlCodeGen.ts | 11 +++- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/packages/internal/src/__tests__/__snapshots__/graphqlCodeGen.test.ts.snap b/packages/internal/src/__tests__/__snapshots__/graphqlCodeGen.test.ts.snap index b9ae584a55ce..800c3c58b24b 100644 --- a/packages/internal/src/__tests__/__snapshots__/graphqlCodeGen.test.ts.snap +++ b/packages/internal/src/__tests__/__snapshots__/graphqlCodeGen.test.ts.snap @@ -11,7 +11,9 @@ export type MakeMaybe = Omit & { [SubKey in K]: Mayb export type ResolverFn = ( args?: TArgs, obj?: { root: TParent; context: TContext; info: GraphQLResolveInfo } - ) => Promise> | Partial; + ) => TResult extends PromiseLike + ? Promise>> + : Promise> | Partial; export type RequireFields = Omit & { [P in K]-?: NonNullable }; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { @@ -195,22 +197,22 @@ export interface JSONObjectScalarConfig extends GraphQLScalarTypeConfig = { - createTodo?: Resolver, ParentType, ContextType, RequireFields>; - renameTodo?: Resolver, ParentType, ContextType, RequireFields>; - updateTodoStatus?: Resolver, ParentType, ContextType, RequireFields>; + createTodo: Resolver, ParentType, ContextType, RequireFields>; + renameTodo: Resolver, ParentType, ContextType, RequireFields>; + updateTodoStatus: Resolver, ParentType, ContextType, RequireFields>; }; export type QueryResolvers = { - currentUser?: Resolver, ParentType, ContextType>; - redwood?: Resolver, ParentType, ContextType>; - todos?: Resolver>>, ParentType, ContextType>; - todosCount?: Resolver; + currentUser: Resolver, ParentType, ContextType>; + redwood: Resolver, ParentType, ContextType>; + todos: Resolver>>, ParentType, ContextType>; + todosCount: Resolver; }; export type RedwoodResolvers = { - currentUser?: Resolver, ParentType, ContextType>; - prismaVersion?: Resolver, ParentType, ContextType>; - version?: Resolver, ParentType, ContextType>; + currentUser: Resolver, ParentType, ContextType>; + prismaVersion: Resolver, ParentType, ContextType>; + version: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }; @@ -219,28 +221,28 @@ export interface TimeScalarConfig extends GraphQLScalarTypeConfig = { - body?: Resolver; - id?: Resolver; - status?: Resolver; + body: Resolver; + id: Resolver; + status: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; export type Resolvers = { - BigInt?: GraphQLScalarType; - Date?: GraphQLScalarType; - DateTime?: GraphQLScalarType; - JSON?: GraphQLScalarType; - JSONObject?: GraphQLScalarType; - Mutation?: MutationResolvers; - Query?: QueryResolvers; - Redwood?: RedwoodResolvers; - Time?: GraphQLScalarType; - Todo?: TodoResolvers; + BigInt: GraphQLScalarType; + Date: GraphQLScalarType; + DateTime: GraphQLScalarType; + JSON: GraphQLScalarType; + JSONObject: GraphQLScalarType; + Mutation: MutationResolvers; + Query: QueryResolvers; + Redwood: RedwoodResolvers; + Time: GraphQLScalarType; + Todo: TodoResolvers; }; export type DirectiveResolvers = { - requireAuth?: requireAuthDirectiveResolver; - skipAuth?: skipAuthDirectiveResolver; + requireAuth: requireAuthDirectiveResolver; + skipAuth: skipAuthDirectiveResolver; }; " `; diff --git a/packages/internal/src/__tests__/resolverFn.test.ts b/packages/internal/src/__tests__/resolverFn.test.ts index 651a394cc298..8a7a76dc5f36 100644 --- a/packages/internal/src/__tests__/resolverFn.test.ts +++ b/packages/internal/src/__tests__/resolverFn.test.ts @@ -39,7 +39,9 @@ describe('ResovlerFn types', () => { "( args?: TArgs, obj?: { root: TParent; context: TContext; info: GraphQLResolveInfo } - ) => Promise> | Partial;" + ) => TResult extends PromiseLike + ? Promise>> + : Promise> | Partial;" `) }) @@ -58,7 +60,9 @@ describe('ResovlerFn types', () => { "( args?: TArgs, obj?: { root: TParent; context: TContext; info: GraphQLResolveInfo } - ) => Promise> | Partial;" + ) => TResult extends PromiseLike + ? Promise>> + : Promise> | Partial;" `) }) @@ -77,8 +81,10 @@ describe('ResovlerFn types', () => { expect(getResolverFnType()).toMatchInlineSnapshot(` "( args: TArgs, - obj: { root: TParent; context: TContext; info: GraphQLResolveInfo } - ) => Promise> | Partial;" + obj?: { root: TParent; context: TContext; info: GraphQLResolveInfo } + ) => TResult extends PromiseLike + ? Promise>> + : Promise> | Partial;" `) }) }) diff --git a/packages/internal/src/generate/graphqlCodeGen.ts b/packages/internal/src/generate/graphqlCodeGen.ts index d33b3a7bfedb..0739ac7bdc16 100644 --- a/packages/internal/src/generate/graphqlCodeGen.ts +++ b/packages/internal/src/generate/graphqlCodeGen.ts @@ -157,6 +157,9 @@ function getPluginConfig() { showUnusedMappers: false, customResolverFn: getResolverFnType(), mappers: prismaModels, + avoidOptionals: { + resolvers: true, + }, contextType: `@redwoodjs/graphql-server/dist/functions/types#RedwoodGraphQLContext`, } @@ -171,12 +174,16 @@ export const getResolverFnType = () => { return `( args: TArgs, obj?: { root: TParent; context: TContext; info: GraphQLResolveInfo } - ) => Promise> | Partial;` + ) => TResult extends PromiseLike + ? Promise>> + : Promise> | Partial;` } else { return `( args?: TArgs, obj?: { root: TParent; context: TContext; info: GraphQLResolveInfo } - ) => Promise> | Partial;` + ) => TResult extends PromiseLike + ? Promise>> + : Promise> | Partial;` } } From e8a1e097ddadbfa83f37149843dc76a53d85afbd Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Mon, 16 May 2022 16:01:35 +0700 Subject: [PATCH 08/64] Expose Scenario data type Update scenario template --- .../service/templates/scenarios.ts.template | 6 ++-- packages/testing/src/api/scenario.ts | 28 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/generate/service/templates/scenarios.ts.template b/packages/cli/src/commands/generate/service/templates/scenarios.ts.template index bb83e1fe33fd..3e3738b3d413 100644 --- a/packages/cli/src/commands/generate/service/templates/scenarios.ts.template +++ b/packages/cli/src/commands/generate/service/templates/scenarios.ts.template @@ -1,5 +1,7 @@ -import type { Prisma } from '@prisma/client' +import type { Prisma, ${prismaTypeName} } from '@prisma/client' +import type { ScenarioData } from '@redwoodjs/testing/api' + export const standard = defineScenario(${stringifiedScenario}) -export type StandardScenario = typeof standard +export type StandardScenario = ScenarioData<'${prismaTypeName.toLowerCase()}', ${prismaTypeName}> diff --git a/packages/testing/src/api/scenario.ts b/packages/testing/src/api/scenario.ts index eb0e571be9ea..910d3122112c 100644 --- a/packages/testing/src/api/scenario.ts +++ b/packages/testing/src/api/scenario.ts @@ -41,6 +41,34 @@ export interface DefineScenario { ): Record>> } +/** + * Type of data seeded by the scenario. Use this type to get the 'strict' return type of defineScenario + * + * @example + * import { Product } from '@prisma/client' + * + * // If you scenario looks like this: + * * export const standard = defineScenario({ + * product: { + * shirt: { + * id: 55, + * price: 10 + * } + * }) + * + * // Export the StandardScenario type as + * export StandardScenario = ScenarioSeed + * + * // You can also define each of the keys in your scenario, so you get stricter type checking + * export StandardScenario = ScenarioSeed + * + */ +export type ScenarioData< + TName extends string | number, // name of the prisma model + TModel, // the prisma model, imported from @prisma/client + TKeys extends string | number = string // (optional) each of the keys in your defineScenario e.g. shirt +> = Record> + interface TestFunctionWithScenario { (scenario?: TData): Promise } From 4ceb0b87ba2d1650b881dca7db2845b3a1e8d0cc Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Mon, 16 May 2022 17:54:59 +0700 Subject: [PATCH 09/64] Update scenario, test and service templates --- .../function/templates/scenarios.ts.template | 4 +- .../__tests__/__snapshots__/sdl.test.js.snap | 28 +++++--- .../__snapshots__/service.test.js.snap | 66 +++++++++++-------- .../src/commands/generate/service/service.js | 3 +- .../service/templates/scenarios.ts.template | 6 +- .../service/templates/service.ts.template | 4 +- .../service/templates/test.ts.template | 6 +- packages/testing/src/api/scenario.ts | 8 +-- 8 files changed, 77 insertions(+), 48 deletions(-) diff --git a/packages/cli/src/commands/generate/function/templates/scenarios.ts.template b/packages/cli/src/commands/generate/function/templates/scenarios.ts.template index 6822bd69c64a..d24ff747f6bb 100644 --- a/packages/cli/src/commands/generate/function/templates/scenarios.ts.template +++ b/packages/cli/src/commands/generate/function/templates/scenarios.ts.template @@ -1,6 +1,8 @@ +import type { ScenarioData } from '@redwoodjs/testing/api' + export const standard = defineScenario({ // Define the "fixture" to write into your test database here // See guide: https://redwoodjs.com/docs/testing#scenarios }) -export type StandardScenario = typeof standard +export type StandardScenario = ScenarioData diff --git a/packages/cli/src/commands/generate/sdl/__tests__/__snapshots__/sdl.test.js.snap b/packages/cli/src/commands/generate/sdl/__tests__/__snapshots__/sdl.test.js.snap index 9ebb851276f3..72c710e57948 100644 --- a/packages/cli/src/commands/generate/sdl/__tests__/__snapshots__/sdl.test.js.snap +++ b/packages/cli/src/commands/generate/sdl/__tests__/__snapshots__/sdl.test.js.snap @@ -139,8 +139,8 @@ export const deleteUser = ({ id }) => { } export const User = { - profiles: (_obj, { root }) => - db.user.findUnique({ where: { id: root.id } }).profiles(), + profiles: (_obj, gqlArgs) => + db.user.findUnique({ where: { id: gqlArgs?.root?.id } }).profiles(), } ", "filePath": "/path/to/project/api/src/services/users/users.js", @@ -231,7 +231,10 @@ describe('customDatums', () => { }) scenario('updates a customData', async (scenario) => { - const original = await customData({ id: scenario.customData.one.id }) + const original = await customData({ + id: scenario.customData.one.id, + }) + const result = await updateCustomData({ id: original.id, input: { data: 'String2' }, @@ -241,7 +244,10 @@ describe('customDatums', () => { }) scenario('deletes a customData', async (scenario) => { - const original = await deleteCustomData({ id: scenario.customData.one.id }) + const original = await deleteCustomData({ + id: scenario.customData.one.id, + }) + const result = await customData({ id: original.id }) expect(result).toEqual(null) @@ -428,8 +434,8 @@ export const deleteUser = ({ id }) => { } export const User = { - profiles: (_obj, { root }) => - db.user.findUnique({ where: { id: root.id } }).profiles(), + profiles: (_obj, gqlArgs) => + db.user.findUnique({ where: { id: gqlArgs?.root?.id } }).profiles(), } ", "filePath": "/path/to/project/api/src/services/users/users.js", @@ -520,7 +526,10 @@ describe('customDatums', () => { }) scenario('updates a customData', async (scenario) => { - const original = await customData({ id: scenario.customData.one.id }) + const original = await customData({ + id: scenario.customData.one.id, + }) + const result = await updateCustomData({ id: original.id, input: { data: 'String2' }, @@ -530,7 +539,10 @@ describe('customDatums', () => { }) scenario('deletes a customData', async (scenario) => { - const original = await deleteCustomData({ id: scenario.customData.one.id }) + const original = await deleteCustomData({ + id: scenario.customData.one.id, + }) + const result = await customData({ id: original.id }) expect(result).toEqual(null) diff --git a/packages/cli/src/commands/generate/service/__tests__/__snapshots__/service.test.js.snap b/packages/cli/src/commands/generate/service/__tests__/__snapshots__/service.test.js.snap index b5b3642a986f..7ed247966955 100644 --- a/packages/cli/src/commands/generate/service/__tests__/__snapshots__/service.test.js.snap +++ b/packages/cli/src/commands/generate/service/__tests__/__snapshots__/service.test.js.snap @@ -65,7 +65,10 @@ describe('transactions', () => { }) scenario('updates a transaction', async (scenario) => { - const original = await transaction({ id: scenario.transaction.one.id }) + const original = await transaction({ + id: scenario.transaction.one.id, + }) + const result = await updateTransaction({ id: original.id, input: { userId: scenario.transaction.two.userId }, @@ -169,8 +172,8 @@ export const user = ({ id }) => { } export const User = { - identity: (_obj, { root }) => - db.user.findUnique({ where: { id: root.id } }).identity(), + identity: (_obj, gqlArgs) => + db.user.findUnique({ where: { id: gqlArgs?.root?.id } }).identity(), } " `; @@ -189,8 +192,8 @@ export const user = ({ id }) => { } export const User = { - userProfiles: (_obj, { root }) => - db.user.findUnique({ where: { id: root.id } }).userProfiles(), + userProfiles: (_obj, gqlArgs) => + db.user.findUnique({ where: { id: gqlArgs?.root?.id } }).userProfiles(), } " `; @@ -209,10 +212,10 @@ export const user = ({ id }) => { } export const User = { - userProfiles: (_obj, { root }) => - db.user.findUnique({ where: { id: root.id } }).userProfiles(), - identity: (_obj, { root }) => - db.user.findUnique({ where: { id: root.id } }).identity(), + userProfiles: (_obj, gqlArgs) => + db.user.findUnique({ where: { id: gqlArgs?.root?.id } }).userProfiles(), + identity: (_obj, gqlArgs) => + db.user.findUnique({ where: { id: gqlArgs?.root?.id } }).identity(), } " `; @@ -289,7 +292,9 @@ export const userProfiles: QueryResolvers['userProfiles'] = () => { exports[`in typescript mode creates a multi word service test file 1`] = ` "import { userProfiles } from './userProfiles' + import type { StandardScenario } from './userProfiles.scenarios' +import type { UserProfile } from '@prisma/client' // Generated boilerplate tests do not account for all circumstances // and can fail without adjustments, e.g. Float and DateTime types. @@ -315,7 +320,9 @@ exports[`in typescript mode creates a multi word service test file with crud act updateTransaction, deleteTransaction, } from './transactions' + import type { StandardScenario } from './transactions.scenarios' +import type { Transaction } from '@prisma/client' // Generated boilerplate tests do not account for all circumstances // and can fail without adjustments, e.g. Float and DateTime types. @@ -348,7 +355,9 @@ describe('transactions', () => { }) scenario('updates a transaction', async (scenario: StandardScenario) => { - const original = await transaction({ id: scenario.transaction.one.id }) + const original = (await transaction({ + id: scenario.transaction.one.id, + })) as Transaction const result = await updateTransaction({ id: original.id, input: { userId: scenario.transaction.two.userId }, @@ -358,9 +367,9 @@ describe('transactions', () => { }) scenario('deletes a transaction', async (scenario: StandardScenario) => { - const original = await deleteTransaction({ + const original = (await deleteTransaction({ id: scenario.transaction.one.id, - }) + })) as Transaction const result = await transaction({ id: original.id }) expect(result).toEqual(null) @@ -453,9 +462,9 @@ export const user: QueryResolvers['user'] = ({ id }) => { }) } -export const User: UserResolvers = { - identity: (_obj, { root }) => - db.user.findUnique({ where: { id: root.id } }).identity(), +export const User: Partial = { + identity: (_obj, gqlArgs) => + db.user.findUnique({ where: { id: gqlArgs?.root?.id } }).identity(), } " `; @@ -474,9 +483,9 @@ export const user: QueryResolvers['user'] = ({ id }) => { }) } -export const User: UserResolvers = { - userProfiles: (_obj, { root }) => - db.user.findUnique({ where: { id: root.id } }).userProfiles(), +export const User: Partial = { + userProfiles: (_obj, gqlArgs) => + db.user.findUnique({ where: { id: gqlArgs?.root?.id } }).userProfiles(), } " `; @@ -495,17 +504,18 @@ export const user: QueryResolvers['user'] = ({ id }) => { }) } -export const User: UserResolvers = { - userProfiles: (_obj, { root }) => - db.user.findUnique({ where: { id: root.id } }).userProfiles(), - identity: (_obj, { root }) => - db.user.findUnique({ where: { id: root.id } }).identity(), +export const User: Partial = { + userProfiles: (_obj, gqlArgs) => + db.user.findUnique({ where: { id: gqlArgs?.root?.id } }).userProfiles(), + identity: (_obj, gqlArgs) => + db.user.findUnique({ where: { id: gqlArgs?.root?.id } }).identity(), } " `; exports[`in typescript mode creates a single word service scenario file 1`] = ` -"import type { Prisma } from '@prisma/client' +"import type { Prisma, User } from '@prisma/client' +import type { ScenarioData } from '@redwoodjs/testing/api' export const standard = defineScenario({ user: { @@ -514,13 +524,15 @@ export const standard = defineScenario({ }, }) -export type StandardScenario = typeof standard +export type StandardScenario = ScenarioData " `; exports[`in typescript mode creates a single word service test file 1`] = ` "import { users, user, createUser, updateUser, deleteUser } from './users' + import type { StandardScenario } from './users.scenarios' +import type { User } from '@prisma/client' // Generated boilerplate tests do not account for all circumstances // and can fail without adjustments, e.g. Float and DateTime types. @@ -550,7 +562,7 @@ describe('users', () => { }) scenario('updates a user', async (scenario: StandardScenario) => { - const original = await user({ id: scenario.user.one.id }) + const original = (await user({ id: scenario.user.one.id })) as User const result = await updateUser({ id: original.id, input: { email: 'String12345672' }, @@ -560,7 +572,7 @@ describe('users', () => { }) scenario('deletes a user', async (scenario: StandardScenario) => { - const original = await deleteUser({ id: scenario.user.one.id }) + const original = (await deleteUser({ id: scenario.user.one.id })) as User const result = await user({ id: original.id }) expect(result).toEqual(null) diff --git a/packages/cli/src/commands/generate/service/service.js b/packages/cli/src/commands/generate/service/service.js index 7b06d4c01550..4cc916b62154 100644 --- a/packages/cli/src/commands/generate/service/service.js +++ b/packages/cli/src/commands/generate/service/service.js @@ -283,6 +283,7 @@ export const files = async ({ relations: relations || [], create: await fieldsToInput(model), update: await fieldsToUpdate(model), + prismaModel: model, ...rest, }, }) @@ -297,7 +298,7 @@ export const files = async ({ templateVars: { scenario: await buildScenario(model), stringifiedScenario: await buildStringifiedScenario(model), - prismaTypeName: `${model}CreateArgs`, + prismaModel: model, ...rest, }, }) diff --git a/packages/cli/src/commands/generate/service/templates/scenarios.ts.template b/packages/cli/src/commands/generate/service/templates/scenarios.ts.template index 3e3738b3d413..0777836cd765 100644 --- a/packages/cli/src/commands/generate/service/templates/scenarios.ts.template +++ b/packages/cli/src/commands/generate/service/templates/scenarios.ts.template @@ -1,7 +1,7 @@ -import type { Prisma, ${prismaTypeName} } from '@prisma/client' +import type { Prisma, ${prismaModel} } from '@prisma/client' import type { ScenarioData } from '@redwoodjs/testing/api' -export const standard = defineScenario(${stringifiedScenario}) +export const standard = defineScenario(${stringifiedScenario}) -export type StandardScenario = ScenarioData<'${prismaTypeName.toLowerCase()}', ${prismaTypeName}> +export type StandardScenario = ScenarioData<${prismaModel}, '${prismaModel.toLowerCase()}'> diff --git a/packages/cli/src/commands/generate/service/templates/service.ts.template b/packages/cli/src/commands/generate/service/templates/service.ts.template index e1ac26f3b1db..498064e5e191 100644 --- a/packages/cli/src/commands/generate/service/templates/service.ts.template +++ b/packages/cli/src/commands/generate/service/templates/service.ts.template @@ -30,6 +30,6 @@ export const delete${singularPascalName}: MutationResolvers['delete${singularPas }) }<% } %><% if (relations.length) { %> -export const ${singularPascalName}: ${singularPascalName}Resolvers = {<% relations.forEach(relation => { %> - ${relation}: (_obj, { root }) => db.${singularCamelName}.findUnique({ where: { id: root.id } }).${relation}(),<% }) %> +export const ${singularPascalName}: Partial<${singularPascalName}Resolvers> = {<% relations.forEach(relation => { %> + ${relation}: (_obj, gqlArgs) => db.${singularCamelName}.findUnique({ where: { id: gqlArgs?.root?.id } }).${relation}(),<% }) %> }<% } %> diff --git a/packages/cli/src/commands/generate/service/templates/test.ts.template b/packages/cli/src/commands/generate/service/templates/test.ts.template index a4f56f9fdb8d..c7e3d7e15c7b 100644 --- a/packages/cli/src/commands/generate/service/templates/test.ts.template +++ b/packages/cli/src/commands/generate/service/templates/test.ts.template @@ -13,7 +13,9 @@ const stringifyButNotScenario = (obj) => { }) } %> import { ${pluralCamelName}<% if (crud) { %>,${singularCamelName}, create${singularPascalName}, update${singularPascalName}, delete${singularPascalName}<% } %> } from './${pluralCamelName}' + import type { StandardScenario } from './${pluralCamelName}.scenarios' +import type { ${prismaModel} } from '@prisma/client' // Generated boilerplate tests do not account for all circumstances // and can fail without adjustments, e.g. Float and DateTime types. @@ -44,7 +46,7 @@ describe('${pluralCamelName}', () => { })<% } %> <% if (update) { %>scenario('updates a ${singularCamelName}', async (scenario: StandardScenario) => {<% rand = parseInt(Math.random() * 10000000) %> - const original = await ${singularCamelName}({ id: scenario.${singularCamelName}.one.id }) + const original = await (${singularCamelName}({ id: scenario.${singularCamelName}.one.id })) as ${prismaModel} const result = await update${singularPascalName}({ id: original.id, input: ${stringifyButNotScenario(update)}, @@ -55,7 +57,7 @@ describe('${pluralCamelName}', () => { })<% } %> scenario('deletes a ${singularCamelName}', async (scenario: StandardScenario) => { - const original = await delete${singularPascalName}({ id: scenario.${singularCamelName}.one.id }) + const original = (await delete${singularPascalName}({ id: scenario.${singularCamelName}.one.id })) as ${prismaModel} const result = await ${singularCamelName}({ id: original.id }) expect(result).toEqual(null) diff --git a/packages/testing/src/api/scenario.ts b/packages/testing/src/api/scenario.ts index 910d3122112c..6abe1fd2a7d6 100644 --- a/packages/testing/src/api/scenario.ts +++ b/packages/testing/src/api/scenario.ts @@ -63,10 +63,10 @@ export interface DefineScenario { * export StandardScenario = ScenarioSeed * */ -export type ScenarioData< - TName extends string | number, // name of the prisma model - TModel, // the prisma model, imported from @prisma/client - TKeys extends string | number = string // (optional) each of the keys in your defineScenario e.g. shirt +export declare type ScenarioData< + TModel, // the prisma model, imported from @prisma/client e.g. "Product" + TName extends string | number = string | number, // (optional) name of the prisma model e.g. "product" + TKeys extends string | number = string | number // (optional) name of each of the seeded scenarios e.g. "shirt" > = Record> interface TestFunctionWithScenario { From b1968fd5d79d7f26c740e957a78d8e02c92e23c4 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Tue, 17 May 2022 12:39:13 +0700 Subject: [PATCH 10/64] Update dbAuth templates for function and src/lib/auth --- .../auth/templates/dbAuth.auth.ts.template | 7 +++++-- .../auth/templates/dbAuth.function.ts.template | 17 ++++++++++++----- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/cli/src/commands/setup/auth/templates/dbAuth.auth.ts.template b/packages/cli/src/commands/setup/auth/templates/dbAuth.auth.ts.template index 63de33f1deb3..5019a89a2be7 100644 --- a/packages/cli/src/commands/setup/auth/templates/dbAuth.auth.ts.template +++ b/packages/cli/src/commands/setup/auth/templates/dbAuth.auth.ts.template @@ -1,6 +1,9 @@ import { AuthenticationError, ForbiddenError } from '@redwoodjs/graphql-server' import { db } from './db' +import type { DbAuthSession } from '@redwoodjs/api' + + /** * The session object sent in as the first argument to getCurrentUser() will * have a single key `id` containing the unique ID of the logged in user @@ -18,7 +21,7 @@ import { db } from './db' * fields to the `select` object below once you've decided they are safe to be * seen if someone were to open the Web Inspector in their browser. */ -export const getCurrentUser = async (session) => { +export const getCurrentUser = async (session: DbAuthSession) => { return await db.user.findUnique({ where: { id: session.id }, select: { id: true }, @@ -71,7 +74,7 @@ export const hasRole = (roles: AllowedRoles): boolean => { return currentUserRoles?.some((allowedRole) => roles.includes(allowedRole) ) - } else if (typeof context.currentUser.roles === 'string') { + } else if (typeof context?.currentUser?.roles === 'string') { // roles to check is an array, currentUser.roles is a string return roles.some( (allowedRole) => context.currentUser?.roles === allowedRole diff --git a/packages/cli/src/commands/setup/auth/templates/dbAuth.function.ts.template b/packages/cli/src/commands/setup/auth/templates/dbAuth.function.ts.template index ee9e7bafa7a2..5c9e5179153e 100644 --- a/packages/cli/src/commands/setup/auth/templates/dbAuth.function.ts.template +++ b/packages/cli/src/commands/setup/auth/templates/dbAuth.function.ts.template @@ -1,9 +1,16 @@ import { db } from 'src/lib/db' import { DbAuthHandler } from '@redwoodjs/api' -export const handler = async (event, context) => { +import type { APIGatewayProxyEvent, Context } from 'aws-lambda' +import type { DbAuthHandlerOptions } from '@redwoodjs/api' - const forgotPasswordOptions = { + +export const handler = async ( + event: APIGatewayProxyEvent, + context: Context +) => { + + const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = { // handler() is invoked after verifying that a user was found with the given // username. This is where you can send the user an email with a link to // reset their password. With the default dbAuth routes and field names, the @@ -33,7 +40,7 @@ export const handler = async (event, context) => { }, } - const loginOptions = { + const loginOptions: DbAuthHandlerOptions['login'] = { // handler() is called after finding the user that matches the // username/password provided at login, but before actually considering them // logged in. The `user` argument will be the user in the database that @@ -62,7 +69,7 @@ export const handler = async (event, context) => { expires: 60 * 60 * 24 * 365 * 10, } - const resetPasswordOptions = { + const resetPasswordOptions: DbAuthHandlerOptions['resetPassword'] = { // handler() is invoked after the password has been successfully updated in // the database. Returning anything truthy will automatically logs the user // in. Return `false` otherwise, and in the Reset Password page redirect the @@ -86,7 +93,7 @@ export const handler = async (event, context) => { }, } - const signupOptions = { + const signupOptions: DbAuthHandlerOptions['signup'] = { // Whatever you want to happen to your data on new user signup. Redwood will // check for duplicate usernames before calling this handler. At a minimum // you need to save the `username`, `hashedPassword` and `salt` to your From 8f435589c438204ef8bd140236c9c80ac29a2360 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Tue, 17 May 2022 14:43:30 +0700 Subject: [PATCH 11/64] Scenario types: Use camel name for prisma model name --- .../commands/generate/service/templates/scenarios.ts.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/generate/service/templates/scenarios.ts.template b/packages/cli/src/commands/generate/service/templates/scenarios.ts.template index 0777836cd765..9b19d5e6dfbf 100644 --- a/packages/cli/src/commands/generate/service/templates/scenarios.ts.template +++ b/packages/cli/src/commands/generate/service/templates/scenarios.ts.template @@ -4,4 +4,4 @@ import type { ScenarioData } from '@redwoodjs/testing/api' export const standard = defineScenario(${stringifiedScenario}) -export type StandardScenario = ScenarioData<${prismaModel}, '${prismaModel.toLowerCase()}'> +export type StandardScenario = ScenarioData<${prismaModel}, '${camelName}'> From 2e58a4c0adb1336609f5abe3d23a9df46bed5141 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Tue, 17 May 2022 17:06:48 +0700 Subject: [PATCH 12/64] Give DbAuthSession number generic by default in generator --- .../src/commands/setup/auth/templates/dbAuth.auth.ts.template | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/commands/setup/auth/templates/dbAuth.auth.ts.template b/packages/cli/src/commands/setup/auth/templates/dbAuth.auth.ts.template index 5019a89a2be7..2a73ca28cd71 100644 --- a/packages/cli/src/commands/setup/auth/templates/dbAuth.auth.ts.template +++ b/packages/cli/src/commands/setup/auth/templates/dbAuth.auth.ts.template @@ -21,7 +21,7 @@ import type { DbAuthSession } from '@redwoodjs/api' * fields to the `select` object below once you've decided they are safe to be * seen if someone were to open the Web Inspector in their browser. */ -export const getCurrentUser = async (session: DbAuthSession) => { +export const getCurrentUser = async (session: DbAuthSession) => { return await db.user.findUnique({ where: { id: session.id }, select: { id: true }, From fc6a38363c64664344abb8ddd18196b2d6fdc781 Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Tue, 17 May 2022 17:08:55 +0700 Subject: [PATCH 13/64] Optional chaining to all errors in cell templates --- .../cli/src/commands/generate/cell/templates/cell.tsx.template | 2 +- .../src/commands/generate/cell/templates/cellList.tsx.template | 2 +- .../scaffold/templates/components/EditNameCell.tsx.template | 2 +- .../scaffold/templates/components/NameCell.tsx.template | 2 +- .../scaffold/templates/components/NamesCell.tsx.template | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/commands/generate/cell/templates/cell.tsx.template b/packages/cli/src/commands/generate/cell/templates/cell.tsx.template index 615463c6a9ef..626a1372167b 100644 --- a/packages/cli/src/commands/generate/cell/templates/cell.tsx.template +++ b/packages/cli/src/commands/generate/cell/templates/cell.tsx.template @@ -16,7 +16,7 @@ export const Empty = () =>
Empty
export const Failure = ({ error, }: CellFailureProps<${operationName}Variables>) => ( -
Error: {error.message}
+
Error: {error?.message}
) export const Success = ({ diff --git a/packages/cli/src/commands/generate/cell/templates/cellList.tsx.template b/packages/cli/src/commands/generate/cell/templates/cellList.tsx.template index 3bc592c867ca..6b7b71094c35 100644 --- a/packages/cli/src/commands/generate/cell/templates/cellList.tsx.template +++ b/packages/cli/src/commands/generate/cell/templates/cellList.tsx.template @@ -15,7 +15,7 @@ export const Loading = () =>
Loading...
export const Empty = () =>
Empty
export const Failure = ({ error }: CellFailureProps) => ( -
Error: {error.message}
+
Error: {error?.message}
) export const Success = ({ ${camelName} }: CellSuccessProps<${operationName}>) => { diff --git a/packages/cli/src/commands/generate/scaffold/templates/components/EditNameCell.tsx.template b/packages/cli/src/commands/generate/scaffold/templates/components/EditNameCell.tsx.template index cd38a2095101..37dba7daa98a 100644 --- a/packages/cli/src/commands/generate/scaffold/templates/components/EditNameCell.tsx.template +++ b/packages/cli/src/commands/generate/scaffold/templates/components/EditNameCell.tsx.template @@ -25,7 +25,7 @@ const UPDATE_${singularConstantName}_MUTATION = gql` export const Loading = () =>
Loading...
export const Failure = ({ error }: CellFailureProps) => ( -
{error.message}
+
{error?.message}
) export const Success = ({ ${singularCamelName} }: CellSuccessProps) => { diff --git a/packages/cli/src/commands/generate/scaffold/templates/components/NameCell.tsx.template b/packages/cli/src/commands/generate/scaffold/templates/components/NameCell.tsx.template index 977f30ddcfbb..5a4e1d05e5b5 100644 --- a/packages/cli/src/commands/generate/scaffold/templates/components/NameCell.tsx.template +++ b/packages/cli/src/commands/generate/scaffold/templates/components/NameCell.tsx.template @@ -16,7 +16,7 @@ export const Loading = () =>
Loading...
export const Empty = () =>
${singularPascalName} not found
export const Failure = ({ error }: CellFailureProps) => ( -
{error.message}
+
{error?.message}
) export const Success = ({ ${singularCamelName} }: CellSuccessProps) => { diff --git a/packages/cli/src/commands/generate/scaffold/templates/components/NamesCell.tsx.template b/packages/cli/src/commands/generate/scaffold/templates/components/NamesCell.tsx.template index 8d7f1cf94d38..da88020df39c 100644 --- a/packages/cli/src/commands/generate/scaffold/templates/components/NamesCell.tsx.template +++ b/packages/cli/src/commands/generate/scaffold/templates/components/NamesCell.tsx.template @@ -30,7 +30,7 @@ export const Empty = () => { } export const Failure = ({ error }: CellFailureProps) => ( -
{error.message}
+
{error?.message}
) export const Success = ({ ${pluralCamelName} }: CellSuccessProps) => { From ba6ed982b8111557b05a487b1fd3b385e9af4dec Mon Sep 17 00:00:00 2001 From: Daniel Choudhury Date: Wed, 18 May 2022 16:32:42 +0700 Subject: [PATCH 14/64] CellSuccessGuarantee + Name, Names, EditName, NewName templates in scaffold --- .../components/EditNameCell.tsx.template | 38 ++++++++++++------- .../templates/components/Name.tsx.template | 28 ++++++++++---- .../templates/components/Names.tsx.template | 23 +++++++---- .../templates/components/NewName.tsx.template | 25 +++++++----- packages/web/src/components/createCell.tsx | 24 +++++++++++- packages/web/src/index.ts | 1 + 6 files changed, 98 insertions(+), 41 deletions(-) diff --git a/packages/cli/src/commands/generate/scaffold/templates/components/EditNameCell.tsx.template b/packages/cli/src/commands/generate/scaffold/templates/components/EditNameCell.tsx.template index 37dba7daa98a..f86b00d20987 100644 --- a/packages/cli/src/commands/generate/scaffold/templates/components/EditNameCell.tsx.template +++ b/packages/cli/src/commands/generate/scaffold/templates/components/EditNameCell.tsx.template @@ -1,4 +1,8 @@ -import type { Edit${singularPascalName}ById } from 'types/graphql' +import type { + Edit${singularPascalName}ById, + Update${singularPascalName}Input, + Update${singularPascalName}MutationVariables +} from 'types/graphql' import type { CellSuccessProps, CellFailureProps } from '@redwoodjs/web' import { useMutation } from '@redwoodjs/web' @@ -29,25 +33,31 @@ export const Failure = ({ error }: CellFailureProps) => ( ) export const Success = ({ ${singularCamelName} }: CellSuccessProps) => { - const [update${singularPascalName}, { loading, error }] = useMutation(UPDATE_${singularConstantName}_MUTATION, { - onCompleted: () => { - toast.success('${singularPascalName} updated') - navigate(routes.${pluralRouteName}()) - }, - onError: (error) => { - toast.error(error.message) - }, - }) - - const onSave = (input, id) => {<% if (intForeignKeys.length) { %> - const castInput = Object.assign(input, { <% intForeignKeys.forEach(key => { %>${key}: parseInt(input.${key}), <% }) %>})<% } %> + const [update${singularPascalName}, { loading, error }] = useMutation( + UPDATE_${singularConstantName}_MUTATION, + { + onCompleted: () => { + toast.success('${singularPascalName} updated') + navigate(routes.${pluralRouteName}()) + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + + const onSave = ( + input: Update${singularPascalName}Input, + id: Update${singularPascalName}MutationVariables + ) => { + <% if (intForeignKeys.length) { %>const castInput = Object.assign(input, { <% intForeignKeys.forEach(key => { %>${key}: parseInt(input.${key}), <% }) %>})<% } %> update${singularPascalName}({ variables: { id, <% if (intForeignKeys.length) { %>input: castInput<% } else { %>input<% } %> } }) } return (
-

Edit ${singularPascalName} {${singularCamelName}.id}

+

Edit ${singularPascalName} {${singularCamelName}?.id}

<${singularPascalName}Form ${singularCamelName}={${singularCamelName}} onSave={onSave} error={error} loading={loading} /> diff --git a/packages/cli/src/commands/generate/scaffold/templates/components/Name.tsx.template b/packages/cli/src/commands/generate/scaffold/templates/components/Name.tsx.template index 2ea89b6bf3d2..7d5fa4fd4dea 100644 --- a/packages/cli/src/commands/generate/scaffold/templates/components/Name.tsx.template +++ b/packages/cli/src/commands/generate/scaffold/templates/components/Name.tsx.template @@ -4,6 +4,14 @@ import { useMutation } from '@redwoodjs/web' import { toast } from '@redwoodjs/web/toast' import { Link, routes, navigate } from '@redwoodjs/router' +import type { CellSuccessData } from '@redwoodjs/web' + + +import type { + Find${singularPascalName}ById, + Delete${singularPascalName}MutationVariables +} from 'types/graphql' + const DELETE_${singularConstantName}_MUTATION = gql` mutation Delete${singularPascalName}Mutation($id: ${idType}!) { delete${singularPascalName}(id: $id) { @@ -23,7 +31,7 @@ const formatEnum = (values: string | string[] | null | undefined) => { } } -const jsonDisplay = (obj) => { +const jsonDisplay = (obj: unknown) => { return (
       {JSON.stringify(obj, null, 2)}
@@ -31,7 +39,7 @@ const jsonDisplay = (obj) => {
   )
 }
 
-const timeTag = (datetime) => {
+const timeTag = (datetime?: string) => {
   return (
     datetime && (