mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-28 11:34:41 +03:00
Implement the new wasp/server/crud
API (#1695)
This commit is contained in:
parent
e2ac659526
commit
cf961f52e2
@ -123,7 +123,8 @@
|
||||
"./client/api": "./dist/api/index.js",
|
||||
"./auth": "./dist/auth/index.js",
|
||||
"./client/auth": "./dist/client/auth/index.js",
|
||||
"./server/auth": "./dist/server/auth/index.js"
|
||||
"./server/auth": "./dist/server/auth/index.js",
|
||||
"./server/crud": "./dist/server/crud/index.js"
|
||||
},
|
||||
{=!
|
||||
TypeScript doesn't care about the redirects we define above in "exports" field; those
|
||||
|
@ -34,13 +34,42 @@ import type {
|
||||
type _WaspEntityTagged = _{= crud.entityUpper =}
|
||||
type _WaspEntity = {= crud.entityUpper =}
|
||||
|
||||
/**
|
||||
* PUBLIC API
|
||||
*/
|
||||
export namespace {= crud.name =} {
|
||||
{=# crud.operations.GetAll =}
|
||||
export type GetAllQuery<Input extends Payload, Output extends Payload> = {= queryType =}<[_WaspEntityTagged], Input, Output>
|
||||
{=/ crud.operations.GetAll =}
|
||||
|
||||
{=# crud.operations.Get =}
|
||||
export type GetQuery<Input extends Payload, Output extends Payload> = {= queryType =}<[_WaspEntityTagged], Input, Output>
|
||||
{=/ crud.operations.Get =}
|
||||
|
||||
{=# crud.operations.Create =}
|
||||
export type CreateAction<Input extends Payload, Output extends Payload>= {= actionType =}<[_WaspEntityTagged], Input, Output>
|
||||
{=/ crud.operations.Create =}
|
||||
|
||||
{=# crud.operations.Update =}
|
||||
export type UpdateAction<Input extends Payload, Output extends Payload> = {= actionType =}<[_WaspEntityTagged], Input, Output>
|
||||
{=/ crud.operations.Update =}
|
||||
|
||||
{=# crud.operations.Delete =}
|
||||
export type DeleteAction<Input extends Payload, Output extends Payload> = {= actionType =}<[_WaspEntityTagged], Input, Output>
|
||||
{=/ crud.operations.Delete =}
|
||||
}
|
||||
|
||||
/**
|
||||
* PRIVATE API
|
||||
*
|
||||
* The types with the `Resolved` suffix are the types that are used internally by the Wasp client
|
||||
* to implement full-stack type safety.
|
||||
*/
|
||||
{=# crud.operations.GetAll =}
|
||||
// Get All query
|
||||
export type GetAllQuery<Input extends Payload, Output extends Payload> = {= queryType =}<[_WaspEntityTagged], Input, Output>
|
||||
{=^ overrides.GetAll.isDefined =}
|
||||
type GetAllInput = {}
|
||||
type GetAllOutput = _WaspEntity[]
|
||||
export type GetAllQueryResolved = GetAllQuery<GetAllInput, GetAllOutput>
|
||||
export type GetAllQueryResolved = {= crud.name =}.GetAllQuery<GetAllInput, GetAllOutput>
|
||||
{=/ overrides.GetAll.isDefined =}
|
||||
{=# overrides.GetAll.isDefined =}
|
||||
const _waspGetAllQuery = {= overrides.GetAll.importIdentifier =}
|
||||
@ -49,12 +78,10 @@ export type GetAllQueryResolved = typeof _waspGetAllQuery
|
||||
{=/ crud.operations.GetAll =}
|
||||
|
||||
{=# crud.operations.Get =}
|
||||
// Get query
|
||||
export type GetQuery<Input extends Payload, Output extends Payload> = {= queryType =}<[_WaspEntityTagged], Input, Output>
|
||||
{=^ overrides.Get.isDefined =}
|
||||
type GetInput = Prisma.{= crud.entityUpper =}WhereUniqueInput
|
||||
type GetOutput = _WaspEntity | null
|
||||
export type GetQueryResolved = GetQuery<GetInput, GetOutput>
|
||||
export type GetQueryResolved = {= crud.name =}.GetQuery<GetInput, GetOutput>
|
||||
{=/ overrides.Get.isDefined =}
|
||||
{=# overrides.Get.isDefined =}
|
||||
const _waspGetQuery = {= overrides.Get.importIdentifier =}
|
||||
@ -63,12 +90,10 @@ export type GetQueryResolved = typeof _waspGetQuery
|
||||
{=/ crud.operations.Get =}
|
||||
|
||||
{=# crud.operations.Create =}
|
||||
// Create action
|
||||
export type CreateAction<Input extends Payload, Output extends Payload>= {= actionType =}<[_WaspEntityTagged], Input, Output>
|
||||
{=^ overrides.Create.isDefined =}
|
||||
type CreateInput = Prisma.{= crud.entityUpper =}CreateInput
|
||||
type CreateOutput = _WaspEntity
|
||||
export type CreateActionResolved = CreateAction<CreateInput, CreateOutput>
|
||||
export type CreateActionResolved = {= crud.name =}.CreateAction<CreateInput, CreateOutput>
|
||||
{=/ overrides.Create.isDefined =}
|
||||
{=# overrides.Create.isDefined =}
|
||||
const _waspCreateAction = {= overrides.Create.importIdentifier =}
|
||||
@ -77,12 +102,10 @@ export type CreateActionResolved = typeof _waspCreateAction
|
||||
{=/ crud.operations.Create =}
|
||||
|
||||
{=# crud.operations.Update =}
|
||||
// Update action
|
||||
export type UpdateAction<Input extends Payload, Output extends Payload> = {= actionType =}<[_WaspEntityTagged], Input, Output>
|
||||
{=^ overrides.Update.isDefined =}
|
||||
type UpdateInput = Prisma.{= crud.entityUpper =}UpdateInput & Prisma.{= crud.entityUpper =}WhereUniqueInput
|
||||
type UpdateOutput = _WaspEntity
|
||||
export type UpdateActionResolved = UpdateAction<UpdateInput, UpdateOutput>
|
||||
export type UpdateActionResolved = {= crud.name =}.UpdateAction<UpdateInput, UpdateOutput>
|
||||
{=/ overrides.Update.isDefined =}
|
||||
{=# overrides.Update.isDefined =}
|
||||
const _waspUpdateAction = {= overrides.Update.importIdentifier =}
|
||||
@ -91,12 +114,10 @@ export type UpdateActionResolved = typeof _waspUpdateAction
|
||||
{=/ crud.operations.Update =}
|
||||
|
||||
{=# crud.operations.Delete =}
|
||||
// Delete action
|
||||
export type DeleteAction<Input extends Payload, Output extends Payload> = {= actionType =}<[_WaspEntityTagged], Input, Output>
|
||||
{=^ overrides.Delete.isDefined =}
|
||||
type DeleteInput = Prisma.{= crud.entityUpper =}WhereUniqueInput
|
||||
type DeleteOutput = _WaspEntity
|
||||
export type DeleteActionResolved = DeleteAction<DeleteInput, DeleteOutput>
|
||||
export type DeleteActionResolved = {= crud.name =}.DeleteAction<DeleteInput, DeleteOutput>
|
||||
{=/ overrides.Delete.isDefined =}
|
||||
{=# overrides.Delete.isDefined =}
|
||||
const _waspDeleteAction = {= overrides.Delete.importIdentifier =}
|
||||
|
5
waspc/data/Generator/templates/sdk/server/crud/index.ts
Normal file
5
waspc/data/Generator/templates/sdk/server/crud/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
{{={= =}=}}
|
||||
|
||||
{=# cruds =}
|
||||
export type { {= name =} } from './{= name =}';
|
||||
{=/ cruds =}
|
@ -0,0 +1,96 @@
|
||||
import { useState, createContext } from 'react'
|
||||
import { createTheme } from '@stitches/react'
|
||||
import { styled } from 'wasp/core/stitches.config'
|
||||
|
||||
import {
|
||||
type State,
|
||||
type CustomizationOptions,
|
||||
type ErrorMessage,
|
||||
type AdditionalSignupFields,
|
||||
} from './types'
|
||||
import { LoginSignupForm } from './internal/common/LoginSignupForm'
|
||||
import { MessageError, MessageSuccess } from './internal/Message'
|
||||
import { ForgotPasswordForm } from './internal/email/ForgotPasswordForm'
|
||||
import { ResetPasswordForm } from './internal/email/ResetPasswordForm'
|
||||
import { VerifyEmailForm } from './internal/email/VerifyEmailForm'
|
||||
|
||||
const logoStyle = {
|
||||
height: '3rem'
|
||||
}
|
||||
|
||||
const Container = styled('div', {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
})
|
||||
|
||||
const HeaderText = styled('h2', {
|
||||
fontSize: '1.875rem',
|
||||
fontWeight: '700',
|
||||
marginTop: '1.5rem'
|
||||
})
|
||||
|
||||
|
||||
// PRIVATE API
|
||||
export const AuthContext = createContext({
|
||||
isLoading: false,
|
||||
setIsLoading: (isLoading: boolean) => {},
|
||||
setErrorMessage: (errorMessage: ErrorMessage | null) => {},
|
||||
setSuccessMessage: (successMessage: string | null) => {},
|
||||
})
|
||||
|
||||
function Auth ({ state, appearance, logo, socialLayout = 'horizontal', additionalSignupFields }: {
|
||||
state: State;
|
||||
} & CustomizationOptions & {
|
||||
additionalSignupFields?: AdditionalSignupFields;
|
||||
}) {
|
||||
const [errorMessage, setErrorMessage] = useState<ErrorMessage | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// TODO(matija): this is called on every render, is it a problem?
|
||||
// If we do it in useEffect(), then there is a glitch between the default color and the
|
||||
// user provided one.
|
||||
const customTheme = createTheme(appearance ?? {})
|
||||
|
||||
const titles: Record<State, string> = {
|
||||
login: 'Log in to your account',
|
||||
signup: 'Create a new account',
|
||||
"forgot-password": "Forgot your password?",
|
||||
"reset-password": "Reset your password",
|
||||
"verify-email": "Email verification",
|
||||
}
|
||||
const title = titles[state]
|
||||
|
||||
const socialButtonsDirection = socialLayout === 'vertical' ? 'vertical' : 'horizontal'
|
||||
|
||||
return (
|
||||
<Container className={customTheme}>
|
||||
<div>
|
||||
{logo && (<img style={logoStyle} src={logo} alt='Your Company' />)}
|
||||
<HeaderText>{title}</HeaderText>
|
||||
</div>
|
||||
|
||||
{errorMessage && (
|
||||
<MessageError>
|
||||
{errorMessage.title}{errorMessage.description && ': '}{errorMessage.description}
|
||||
</MessageError>
|
||||
)}
|
||||
{successMessage && <MessageSuccess>{successMessage}</MessageSuccess>}
|
||||
<AuthContext.Provider value={{ isLoading, setIsLoading, setErrorMessage, setSuccessMessage }}>
|
||||
{(state === 'login' || state === 'signup') && (
|
||||
<LoginSignupForm
|
||||
state={state}
|
||||
socialButtonsDirection={socialButtonsDirection}
|
||||
additionalSignupFields={additionalSignupFields}
|
||||
/>
|
||||
)}
|
||||
{state === 'forgot-password' && (<ForgotPasswordForm />)}
|
||||
{state === 'reset-password' && (<ResetPasswordForm />)}
|
||||
{state === 'verify-email' && (<VerifyEmailForm />)}
|
||||
</AuthContext.Provider>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
// PRIVATE API
|
||||
export default Auth;
|
@ -0,0 +1,18 @@
|
||||
import Auth from './Auth'
|
||||
import { type CustomizationOptions, State } from './types'
|
||||
|
||||
// PUBLIC API
|
||||
export function LoginForm({
|
||||
appearance,
|
||||
logo,
|
||||
socialLayout,
|
||||
}: CustomizationOptions) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.Login}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import Auth from './Auth'
|
||||
import {
|
||||
type CustomizationOptions,
|
||||
type AdditionalSignupFields,
|
||||
State,
|
||||
} from './types'
|
||||
|
||||
// PUBLIC API
|
||||
export function SignupForm({
|
||||
appearance,
|
||||
logo,
|
||||
socialLayout,
|
||||
additionalFields,
|
||||
}: CustomizationOptions & { additionalFields?: AdditionalSignupFields; }) {
|
||||
return (
|
||||
<Auth
|
||||
appearance={appearance}
|
||||
logo={logo}
|
||||
socialLayout={socialLayout}
|
||||
state={State.Signup}
|
||||
additionalSignupFields={additionalFields}
|
||||
/>
|
||||
)
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
import { styled } from 'wasp/core/stitches.config'
|
||||
|
||||
// PRIVATE API
|
||||
export const Form = styled('form', {
|
||||
marginTop: '1.5rem',
|
||||
})
|
||||
|
||||
// PUBLIC API
|
||||
export const FormItemGroup = styled('div', {
|
||||
'& + div': {
|
||||
marginTop: '1.5rem',
|
||||
},
|
||||
})
|
||||
|
||||
// PUBLIC API
|
||||
export const FormLabel = styled('label', {
|
||||
display: 'block',
|
||||
fontSize: '$sm',
|
||||
fontWeight: '500',
|
||||
marginBottom: '0.5rem',
|
||||
})
|
||||
|
||||
const commonInputStyles = {
|
||||
display: 'block',
|
||||
lineHeight: '1.5rem',
|
||||
fontSize: '$sm',
|
||||
borderWidth: '1px',
|
||||
borderColor: '$gray600',
|
||||
backgroundColor: '#f8f4ff',
|
||||
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
'&:focus': {
|
||||
borderWidth: '1px',
|
||||
borderColor: '$gray700',
|
||||
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
backgroundColor: '$gray400',
|
||||
borderColor: '$gray400',
|
||||
color: '$gray500',
|
||||
},
|
||||
|
||||
borderRadius: '0.375rem',
|
||||
width: '100%',
|
||||
|
||||
paddingTop: '0.375rem',
|
||||
paddingBottom: '0.375rem',
|
||||
paddingLeft: '0.75rem',
|
||||
paddingRight: '0.75rem',
|
||||
margin: 0,
|
||||
}
|
||||
|
||||
// PUBLIC API
|
||||
export const FormInput = styled('input', commonInputStyles)
|
||||
|
||||
// PUBLIC API
|
||||
export const FormTextarea = styled('textarea', commonInputStyles)
|
||||
|
||||
// PUBLIC API
|
||||
export const FormError = styled('div', {
|
||||
display: 'block',
|
||||
fontSize: '$sm',
|
||||
fontWeight: '500',
|
||||
color: '$formErrorText',
|
||||
marginTop: '0.5rem',
|
||||
})
|
||||
|
||||
// PRIVATE API
|
||||
export const SubmitButton = styled('button', {
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
|
||||
width: '100%',
|
||||
borderWidth: '1px',
|
||||
borderColor: '$brand',
|
||||
backgroundColor: '$brand',
|
||||
color: '$submitButtonText',
|
||||
|
||||
padding: '0.5rem 0.75rem',
|
||||
boxShadow: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
||||
|
||||
fontWeight: '600',
|
||||
fontSize: '$sm',
|
||||
lineHeight: '1.25rem',
|
||||
borderRadius: '0.375rem',
|
||||
|
||||
// TODO(matija): extract this into separate BaseButton component and then inherit it.
|
||||
'&:hover': {
|
||||
backgroundColor: '$brandAccent',
|
||||
borderColor: '$brandAccent',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
backgroundColor: '$gray400',
|
||||
borderColor: '$gray400',
|
||||
color: '$gray500',
|
||||
},
|
||||
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transitionDuration: '100ms',
|
||||
})
|
@ -0,0 +1,21 @@
|
||||
import { styled } from 'wasp/core/stitches.config'
|
||||
|
||||
// PRIVATE API
|
||||
export const Message = styled('div', {
|
||||
padding: '0.5rem 0.75rem',
|
||||
borderRadius: '0.375rem',
|
||||
marginTop: '1rem',
|
||||
background: '$gray400',
|
||||
})
|
||||
|
||||
// PRIVATE API
|
||||
export const MessageError = styled(Message, {
|
||||
background: '$errorBackground',
|
||||
color: '$errorText',
|
||||
})
|
||||
|
||||
// PRIVATE API
|
||||
export const MessageSuccess = styled(Message, {
|
||||
background: '$successBackground',
|
||||
color: '$successText',
|
||||
})
|
@ -0,0 +1,184 @@
|
||||
import { useContext } from 'react'
|
||||
import { useForm, UseFormReturn } from 'react-hook-form'
|
||||
import { styled } from 'wasp/core/stitches.config'
|
||||
import config from 'wasp/core/config'
|
||||
|
||||
import { AuthContext } from '../../Auth'
|
||||
import {
|
||||
Form,
|
||||
FormInput,
|
||||
FormItemGroup,
|
||||
FormLabel,
|
||||
FormError,
|
||||
FormTextarea,
|
||||
SubmitButton,
|
||||
} from '../Form'
|
||||
import type {
|
||||
AdditionalSignupFields,
|
||||
AdditionalSignupField,
|
||||
AdditionalSignupFieldRenderFn,
|
||||
FormState,
|
||||
} from '../../types'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { useEmail } from '../email/useEmail'
|
||||
|
||||
|
||||
// PRIVATE API
|
||||
export type LoginSignupFormFields = {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
// PRIVATE API
|
||||
export const LoginSignupForm = ({
|
||||
state,
|
||||
socialButtonsDirection = 'horizontal',
|
||||
additionalSignupFields,
|
||||
}: {
|
||||
state: 'login' | 'signup'
|
||||
socialButtonsDirection?: 'horizontal' | 'vertical'
|
||||
additionalSignupFields?: AdditionalSignupFields
|
||||
}) => {
|
||||
const {
|
||||
isLoading,
|
||||
setErrorMessage,
|
||||
setSuccessMessage,
|
||||
setIsLoading,
|
||||
} = useContext(AuthContext)
|
||||
const isLogin = state === 'login'
|
||||
const cta = isLogin ? 'Log in' : 'Sign up';
|
||||
const history = useHistory();
|
||||
const onErrorHandler = (error) => {
|
||||
setErrorMessage({ title: error.message, description: error.data?.data?.message })
|
||||
};
|
||||
const hookForm = useForm<LoginSignupFormFields>()
|
||||
const { register, formState: { errors }, handleSubmit: hookFormHandleSubmit } = hookForm
|
||||
const { handleSubmit } = useEmail({
|
||||
isLogin,
|
||||
onError: onErrorHandler,
|
||||
showEmailVerificationPending() {
|
||||
hookForm.reset()
|
||||
setSuccessMessage(`You've signed up successfully! Check your email for the confirmation link.`)
|
||||
},
|
||||
onLoginSuccess() {
|
||||
history.push('/')
|
||||
},
|
||||
});
|
||||
async function onSubmit (data) {
|
||||
setIsLoading(true);
|
||||
setErrorMessage(null);
|
||||
setSuccessMessage(null);
|
||||
try {
|
||||
await handleSubmit(data);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (<>
|
||||
<Form onSubmit={hookFormHandleSubmit(onSubmit)}>
|
||||
<FormItemGroup>
|
||||
<FormLabel>E-mail</FormLabel>
|
||||
<FormInput
|
||||
{...register('email', {
|
||||
required: 'Email is required',
|
||||
})}
|
||||
type="email"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.email && <FormError>{errors.email.message}</FormError>}
|
||||
</FormItemGroup>
|
||||
<FormItemGroup>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormInput
|
||||
{...register('password', {
|
||||
required: 'Password is required',
|
||||
})}
|
||||
type="password"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors.password && <FormError>{errors.password.message}</FormError>}
|
||||
</FormItemGroup>
|
||||
<AdditionalFormFields
|
||||
hookForm={hookForm}
|
||||
formState={{ isLoading }}
|
||||
additionalSignupFields={additionalSignupFields}
|
||||
/>
|
||||
<FormItemGroup>
|
||||
<SubmitButton type="submit" disabled={isLoading}>{cta}</SubmitButton>
|
||||
</FormItemGroup>
|
||||
</Form>
|
||||
</>)
|
||||
}
|
||||
|
||||
function AdditionalFormFields({
|
||||
hookForm,
|
||||
formState: { isLoading },
|
||||
additionalSignupFields,
|
||||
}: {
|
||||
hookForm: UseFormReturn<LoginSignupFormFields>;
|
||||
formState: FormState;
|
||||
additionalSignupFields: AdditionalSignupFields;
|
||||
}) {
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = hookForm;
|
||||
|
||||
function renderField<ComponentType extends React.JSXElementConstructor<any>>(
|
||||
field: AdditionalSignupField,
|
||||
// Ideally we would use ComponentType here, but it doesn't work with react-hook-form
|
||||
Component: any,
|
||||
props?: React.ComponentProps<ComponentType>
|
||||
) {
|
||||
return (
|
||||
<FormItemGroup key={field.name}>
|
||||
<FormLabel>{field.label}</FormLabel>
|
||||
<Component
|
||||
{...register(field.name, field.validations)}
|
||||
{...props}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{errors[field.name] && (
|
||||
<FormError>{errors[field.name].message}</FormError>
|
||||
)}
|
||||
</FormItemGroup>
|
||||
);
|
||||
}
|
||||
|
||||
if (areAdditionalFieldsRenderFn(additionalSignupFields)) {
|
||||
return additionalSignupFields(hookForm, { isLoading })
|
||||
}
|
||||
|
||||
return (
|
||||
additionalSignupFields &&
|
||||
additionalSignupFields.map((field) => {
|
||||
if (isFieldRenderFn(field)) {
|
||||
return field(hookForm, { isLoading })
|
||||
}
|
||||
switch (field.type) {
|
||||
case 'input':
|
||||
return renderField<typeof FormInput>(field, FormInput, {
|
||||
type: 'text',
|
||||
})
|
||||
case 'textarea':
|
||||
return renderField<typeof FormTextarea>(field, FormTextarea)
|
||||
default:
|
||||
throw new Error(
|
||||
`Unsupported additional signup field type: ${field.type}`
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function isFieldRenderFn(
|
||||
additionalSignupField: AdditionalSignupField | AdditionalSignupFieldRenderFn
|
||||
): additionalSignupField is AdditionalSignupFieldRenderFn {
|
||||
return typeof additionalSignupField === 'function'
|
||||
}
|
||||
|
||||
function areAdditionalFieldsRenderFn(
|
||||
additionalSignupFields: AdditionalSignupFields
|
||||
): additionalSignupFields is AdditionalSignupFieldRenderFn {
|
||||
return typeof additionalSignupFields === 'function'
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
import { createTheme } from '@stitches/react'
|
||||
import { UseFormReturn, RegisterOptions } from 'react-hook-form'
|
||||
import type { LoginSignupFormFields } from './internal/common/LoginSignupForm'
|
||||
|
||||
// PRIVATE API
|
||||
export enum State {
|
||||
Login = 'login',
|
||||
Signup = 'signup',
|
||||
ForgotPassword = 'forgot-password',
|
||||
ResetPassword = 'reset-password',
|
||||
VerifyEmail = 'verify-email',
|
||||
}
|
||||
|
||||
// PUBLIC API
|
||||
export type CustomizationOptions = {
|
||||
logo?: string
|
||||
socialLayout?: 'horizontal' | 'vertical'
|
||||
appearance?: Parameters<typeof createTheme>[0]
|
||||
}
|
||||
|
||||
// PRIVATE API
|
||||
export type ErrorMessage = {
|
||||
title: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
// PRIVATE API
|
||||
export type FormState = {
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
// PRIVATE API
|
||||
export type AdditionalSignupFieldRenderFn = (
|
||||
hookForm: UseFormReturn<LoginSignupFormFields>,
|
||||
formState: FormState
|
||||
) => React.ReactNode
|
||||
|
||||
// PRIVATE API
|
||||
export type AdditionalSignupField = {
|
||||
name: string
|
||||
label: string
|
||||
type: 'input' | 'textarea'
|
||||
validations?: RegisterOptions<LoginSignupFormFields>
|
||||
}
|
||||
|
||||
// PRIVATE API
|
||||
export type AdditionalSignupFields =
|
||||
| (AdditionalSignupField | AdditionalSignupFieldRenderFn)[]
|
||||
| AdditionalSignupFieldRenderFn
|
@ -63,7 +63,8 @@
|
||||
"./client/api": "./dist/api/index.js",
|
||||
"./auth": "./dist/auth/index.js",
|
||||
"./client/auth": "./dist/client/auth/index.js",
|
||||
"./server/auth": "./dist/server/auth/index.js"
|
||||
"./server/auth": "./dist/server/auth/index.js",
|
||||
"./server/crud": "./dist/server/crud/index.js"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Task } from "wasp/entities";
|
||||
import { GetAllQuery } from "wasp/server/crud/Tasks";
|
||||
import { Task } from 'wasp/entities'
|
||||
import { Tasks } from 'wasp/server/crud'
|
||||
|
||||
export const getAllQuery = ((args, context) => {
|
||||
return context.entities.Task.findMany({});
|
||||
}) satisfies GetAllQuery<{}, Task[]>;
|
||||
return context.entities.Task.findMany({})
|
||||
}) satisfies Tasks.GetAllQuery<{}, Task[]>
|
||||
|
@ -45,6 +45,7 @@ import Wasp.Generator.SdkGenerator.JobGenerator (genJobTypes)
|
||||
import Wasp.Generator.SdkGenerator.RouterGenerator (genRouter)
|
||||
import Wasp.Generator.SdkGenerator.RpcGenerator (genRpc)
|
||||
import Wasp.Generator.SdkGenerator.Server.AuthG (genNewServerApi)
|
||||
import Wasp.Generator.SdkGenerator.Server.CrudG (genNewServerCrudApi)
|
||||
import Wasp.Generator.SdkGenerator.ServerApiG (genServerApi)
|
||||
import Wasp.Generator.SdkGenerator.ServerOpsGenerator (genOperations)
|
||||
import Wasp.Generator.SdkGenerator.WebSocketGenerator (depsRequiredByWebSockets, genWebSockets)
|
||||
@ -117,6 +118,7 @@ genSdkReal spec =
|
||||
-- New API
|
||||
<++> genNewClientAuth spec
|
||||
<++> genNewServerApi spec
|
||||
<++> genNewServerCrudApi spec
|
||||
where
|
||||
genFileCopy = return . C.mkTmplFd
|
||||
|
||||
|
33
waspc/src/Wasp/Generator/SdkGenerator/Server/CrudG.hs
Normal file
33
waspc/src/Wasp/Generator/SdkGenerator/Server/CrudG.hs
Normal file
@ -0,0 +1,33 @@
|
||||
module Wasp.Generator.SdkGenerator.Server.CrudG
|
||||
( genNewServerCrudApi,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Aeson (object, (.=))
|
||||
import qualified Data.Aeson as Aeson
|
||||
import StrongPath (relfile)
|
||||
import Wasp.AppSpec (AppSpec, getCruds)
|
||||
import qualified Wasp.AppSpec.Crud as AS.Crud
|
||||
import Wasp.AppSpec.Valid (getIdFieldFromCrudEntity)
|
||||
import Wasp.Generator.Crud (getCrudOperationJson)
|
||||
import Wasp.Generator.FileDraft (FileDraft)
|
||||
import Wasp.Generator.Monad (Generator)
|
||||
import qualified Wasp.Generator.SdkGenerator.Common as C
|
||||
|
||||
genNewServerCrudApi :: AppSpec -> Generator [FileDraft]
|
||||
genNewServerCrudApi spec =
|
||||
if areThereAnyCruds
|
||||
then sequence [genCrudIndex spec cruds]
|
||||
else return []
|
||||
where
|
||||
cruds = getCruds spec
|
||||
areThereAnyCruds = not $ null cruds
|
||||
|
||||
genCrudIndex :: AppSpec -> [(String, AS.Crud.Crud)] -> Generator FileDraft
|
||||
genCrudIndex spec cruds = return $ C.mkTmplFdWithData [relfile|server/crud/index.ts|] tmplData
|
||||
where
|
||||
tmplData = object ["cruds" .= map getCrudOperationJsonFromCrud cruds]
|
||||
getCrudOperationJsonFromCrud :: (String, AS.Crud.Crud) -> Aeson.Value
|
||||
getCrudOperationJsonFromCrud (name, crud) = getCrudOperationJson name crud idField
|
||||
where
|
||||
idField = getIdFieldFromCrudEntity spec crud
|
@ -310,6 +310,7 @@ library
|
||||
Wasp.Generator.SdkGenerator.RouterGenerator
|
||||
Wasp.Generator.SdkGenerator.Client.AuthG
|
||||
Wasp.Generator.SdkGenerator.Server.AuthG
|
||||
Wasp.Generator.SdkGenerator.Server.CrudG
|
||||
Wasp.Generator.ServerGenerator
|
||||
Wasp.Generator.ServerGenerator.JsImport
|
||||
Wasp.Generator.ServerGenerator.ApiRoutesG
|
||||
|
Loading…
Reference in New Issue
Block a user