mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-27 02:52:22 +03:00
Implement Wasp SDK generation (#1626)
This commit is contained in:
parent
bc9e1b6bce
commit
cbd7310b16
@ -22,13 +22,7 @@ import qualified Wasp.Generator
|
|||||||
import Wasp.Generator.Monad (GeneratorWarning (GeneratorNeedsMigrationWarning))
|
import Wasp.Generator.Monad (GeneratorWarning (GeneratorNeedsMigrationWarning))
|
||||||
import qualified Wasp.Message as Msg
|
import qualified Wasp.Message as Msg
|
||||||
import Wasp.Project (CompileError, CompileWarning, WaspProjectDir)
|
import Wasp.Project (CompileError, CompileWarning, WaspProjectDir)
|
||||||
import Wasp.Project.Common
|
import Wasp.Project.Common (buildDirInDotWaspDir, dotWaspDirInWaspProjectDir)
|
||||||
( buildDirInDotWaspDir,
|
|
||||||
dotWaspDirInWaspProjectDir,
|
|
||||||
extClientCodeDirInWaspProjectDir,
|
|
||||||
extServerCodeDirInWaspProjectDir,
|
|
||||||
extSharedCodeDirInWaspProjectDir,
|
|
||||||
)
|
|
||||||
|
|
||||||
-- | Builds Wasp project that the current working directory is part of.
|
-- | Builds Wasp project that the current working directory is part of.
|
||||||
-- Does all the steps, from analysis to generation, and at the end writes generated code
|
-- Does all the steps, from analysis to generation, and at the end writes generated code
|
||||||
@ -71,9 +65,7 @@ buildIO waspProjectDir buildDir = compileIOWithOptions options waspProjectDir bu
|
|||||||
where
|
where
|
||||||
options =
|
options =
|
||||||
CompileOptions
|
CompileOptions
|
||||||
{ externalClientCodeDirPath = waspProjectDir </> extClientCodeDirInWaspProjectDir,
|
{ waspProjectDirPath = waspProjectDir,
|
||||||
externalServerCodeDirPath = waspProjectDir </> extServerCodeDirInWaspProjectDir,
|
|
||||||
externalSharedCodeDirPath = waspProjectDir </> extSharedCodeDirInWaspProjectDir,
|
|
||||||
isBuild = True,
|
isBuild = True,
|
||||||
sendMessage = cliSendMessage,
|
sendMessage = cliSendMessage,
|
||||||
-- Ignore "DB needs migration warnings" during build, as that is not a required step.
|
-- Ignore "DB needs migration warnings" during build, as that is not a required step.
|
||||||
|
@ -26,7 +26,7 @@ import qualified Wasp.Generator
|
|||||||
import qualified Wasp.Message as Msg
|
import qualified Wasp.Message as Msg
|
||||||
import Wasp.Project (CompileError, CompileWarning, WaspProjectDir)
|
import Wasp.Project (CompileError, CompileWarning, WaspProjectDir)
|
||||||
import qualified Wasp.Project
|
import qualified Wasp.Project
|
||||||
import Wasp.Project.Common (dotWaspDirInWaspProjectDir, extClientCodeDirInWaspProjectDir, extServerCodeDirInWaspProjectDir, extSharedCodeDirInWaspProjectDir, generatedCodeDirInDotWaspDir)
|
import Wasp.Project.Common (dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir)
|
||||||
|
|
||||||
-- | Same like 'compileWithOptions', but with default compile options.
|
-- | Same like 'compileWithOptions', but with default compile options.
|
||||||
compile :: Command [CompileWarning]
|
compile :: Command [CompileWarning]
|
||||||
@ -115,9 +115,7 @@ compileIOWithOptions options waspProjectDir outDir =
|
|||||||
defaultCompileOptions :: Path' Abs (Dir WaspProjectDir) -> CompileOptions
|
defaultCompileOptions :: Path' Abs (Dir WaspProjectDir) -> CompileOptions
|
||||||
defaultCompileOptions waspProjectDir =
|
defaultCompileOptions waspProjectDir =
|
||||||
CompileOptions
|
CompileOptions
|
||||||
{ externalServerCodeDirPath = waspProjectDir </> extServerCodeDirInWaspProjectDir,
|
{ waspProjectDirPath = waspProjectDir,
|
||||||
externalClientCodeDirPath = waspProjectDir </> extClientCodeDirInWaspProjectDir,
|
|
||||||
externalSharedCodeDirPath = waspProjectDir </> extSharedCodeDirInWaspProjectDir,
|
|
||||||
isBuild = False,
|
isBuild = False,
|
||||||
sendMessage = cliSendMessage,
|
sendMessage = cliSendMessage,
|
||||||
generatorWarningsFilter = id
|
generatorWarningsFilter = id
|
||||||
|
@ -15,9 +15,12 @@ import Wasp.Cli.Command.Message (cliSendMessageC)
|
|||||||
import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require)
|
import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require)
|
||||||
import Wasp.Cli.Command.Watch (watch)
|
import Wasp.Cli.Command.Watch (watch)
|
||||||
import qualified Wasp.Generator
|
import qualified Wasp.Generator
|
||||||
import Wasp.Generator.Common (ProjectRootDir)
|
|
||||||
import qualified Wasp.Message as Msg
|
import qualified Wasp.Message as Msg
|
||||||
import Wasp.Project.Common (dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir)
|
import Wasp.Project.Common
|
||||||
|
( WaspProjectDir,
|
||||||
|
dotWaspDirInWaspProjectDir,
|
||||||
|
generatedCodeDirInDotWaspDir,
|
||||||
|
)
|
||||||
|
|
||||||
test :: [String] -> Command ()
|
test :: [String] -> Command ()
|
||||||
test [] = throwError $ CommandError "Not enough arguments" "Expected: wasp test client <args>"
|
test [] = throwError $ CommandError "Not enough arguments" "Expected: wasp test client <args>"
|
||||||
@ -25,7 +28,7 @@ test ("client" : args) = watchAndTest $ Wasp.Generator.testWebApp args
|
|||||||
test ("server" : _args) = throwError $ CommandError "Invalid arguments" "Server testing not yet implemented."
|
test ("server" : _args) = throwError $ CommandError "Invalid arguments" "Server testing not yet implemented."
|
||||||
test _ = throwError $ CommandError "Invalid arguments" "Expected: wasp test client <args>"
|
test _ = throwError $ CommandError "Invalid arguments" "Expected: wasp test client <args>"
|
||||||
|
|
||||||
watchAndTest :: (Path' Abs (Dir ProjectRootDir) -> IO (Either String ())) -> Command ()
|
watchAndTest :: (Path' Abs (Dir WaspProjectDir) -> IO (Either String ())) -> Command ()
|
||||||
watchAndTest testRunner = do
|
watchAndTest testRunner = do
|
||||||
InWaspProject waspRoot <- require
|
InWaspProject waspRoot <- require
|
||||||
let outDir = waspRoot </> dotWaspDirInWaspProjectDir </> generatedCodeDirInDotWaspDir
|
let outDir = waspRoot </> dotWaspDirInWaspProjectDir </> generatedCodeDirInDotWaspDir
|
||||||
@ -39,7 +42,7 @@ watchAndTest testRunner = do
|
|||||||
watchOrStartResult <- liftIO $ do
|
watchOrStartResult <- liftIO $ do
|
||||||
ongoingCompilationResultMVar <- newMVar (warnings, [])
|
ongoingCompilationResultMVar <- newMVar (warnings, [])
|
||||||
let watchWaspProjectSource = watch waspRoot outDir ongoingCompilationResultMVar
|
let watchWaspProjectSource = watch waspRoot outDir ongoingCompilationResultMVar
|
||||||
watchWaspProjectSource `race` testRunner outDir
|
watchWaspProjectSource `race` testRunner waspRoot
|
||||||
|
|
||||||
case watchOrStartResult of
|
case watchOrStartResult of
|
||||||
Left () -> error "This should never happen, listening for file changes should never end but it did."
|
Left () -> error "This should never happen, listening for file changes should never end but it did."
|
||||||
|
@ -19,7 +19,7 @@ import Wasp.Cli.Message (cliSendMessage)
|
|||||||
import qualified Wasp.Generator.Common as Wasp.Generator
|
import qualified Wasp.Generator.Common as Wasp.Generator
|
||||||
import qualified Wasp.Message as Msg
|
import qualified Wasp.Message as Msg
|
||||||
import Wasp.Project (CompileError, CompileWarning, WaspProjectDir)
|
import Wasp.Project (CompileError, CompileWarning, WaspProjectDir)
|
||||||
import Wasp.Project.Common (srcDirInWaspProjectDir)
|
import Wasp.Project.Common (extPublicDirInWaspProjectDir, srcDirInWaspProjectDir)
|
||||||
|
|
||||||
-- TODO: Idea: Read .gitignore file, and ignore everything from it. This will then also cover the
|
-- TODO: Idea: Read .gitignore file, and ignore everything from it. This will then also cover the
|
||||||
-- .wasp dir, and users can easily add any custom stuff they want ignored. But, we also have to
|
-- .wasp dir, and users can easily add any custom stuff they want ignored. But, we also have to
|
||||||
@ -39,7 +39,8 @@ watch ::
|
|||||||
watch waspProjectDir outDir ongoingCompilationResultMVar = FSN.withManager $ \mgr -> do
|
watch waspProjectDir outDir ongoingCompilationResultMVar = FSN.withManager $ \mgr -> do
|
||||||
chan <- newChan
|
chan <- newChan
|
||||||
_ <- watchFilesAtTopLevelOfWaspProjectDir mgr chan
|
_ <- watchFilesAtTopLevelOfWaspProjectDir mgr chan
|
||||||
_ <- watchFilesAtAllLevelsOfSrcDirInWaspProjectDir mgr chan
|
_ <- watchFilesAtAllLevelsOfDirInWaspProjectDir mgr chan srcDirInWaspProjectDir
|
||||||
|
_ <- watchFilesAtAllLevelsOfDirInWaspProjectDir mgr chan extPublicDirInWaspProjectDir
|
||||||
listenForEvents chan =<< getCurrentTime
|
listenForEvents chan =<< getCurrentTime
|
||||||
where
|
where
|
||||||
watchFilesAtTopLevelOfWaspProjectDir mgr chan =
|
watchFilesAtTopLevelOfWaspProjectDir mgr chan =
|
||||||
@ -53,8 +54,8 @@ watch waspProjectDir outDir ongoingCompilationResultMVar = FSN.withManager $ \mg
|
|||||||
where
|
where
|
||||||
filename = FP.takeFileName $ FSN.eventPath event
|
filename = FP.takeFileName $ FSN.eventPath event
|
||||||
|
|
||||||
watchFilesAtAllLevelsOfSrcDirInWaspProjectDir mgr chan =
|
watchFilesAtAllLevelsOfDirInWaspProjectDir mgr chan dirInWaspProjectDir =
|
||||||
FSN.watchTreeChan mgr (SP.fromAbsDir $ waspProjectDir </> srcDirInWaspProjectDir) eventFilter chan
|
FSN.watchTreeChan mgr (SP.fromAbsDir $ waspProjectDir </> dirInWaspProjectDir) eventFilter chan
|
||||||
where
|
where
|
||||||
eventFilter :: FSN.Event -> Bool
|
eventFilter :: FSN.Event -> Bool
|
||||||
eventFilter event =
|
eventFilter event =
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
"react": "^18.2.0"
|
"react": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"typescript": "^5.1.0",
|
||||||
|
"vite": "^4.3.9",
|
||||||
"@types/react": "^18.0.37",
|
"@types/react": "^18.0.37",
|
||||||
"prisma": "4.16.2"
|
"prisma": "4.16.2"
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { throwIfNotValidAbsoluteURL } from './universal/validators.mjs';
|
import { throwIfNotValidAbsoluteURL } from 'wasp/universal/validators';
|
||||||
|
|
||||||
console.info("🔍 Validating environment variables...");
|
console.info("🔍 Validating environment variables...");
|
||||||
throwIfNotValidAbsoluteURL(process.env.REACT_APP_API_URL, 'Environemnt variable REACT_APP_API_URL');
|
throwIfNotValidAbsoluteURL(process.env.REACT_APP_API_URL, 'Environemnt variable REACT_APP_API_URL');
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
{{={= =}=}}
|
|
||||||
import { createAction } from './core'
|
|
||||||
{=& operationTypeImportStmt =}
|
|
||||||
|
|
||||||
const action = createAction<{= operationTypeName =}>(
|
|
||||||
'{= actionRoute =}',
|
|
||||||
{=& entitiesArray =},
|
|
||||||
)
|
|
||||||
|
|
||||||
export default action
|
|
@ -1,13 +0,0 @@
|
|||||||
import { type Action } from '.'
|
|
||||||
import type { Expand, _Awaited, _ReturnType } from '../universal/types'
|
|
||||||
|
|
||||||
export function createAction<BackendAction extends GenericBackendAction>(
|
|
||||||
actionRoute: string,
|
|
||||||
entitiesUsed: unknown[]
|
|
||||||
): ActionFor<BackendAction>
|
|
||||||
|
|
||||||
type ActionFor<BackendAction extends GenericBackendAction> = Expand<
|
|
||||||
Action<Parameters<BackendAction>[0], _Awaited<_ReturnType<BackendAction>>>
|
|
||||||
>
|
|
||||||
|
|
||||||
type GenericBackendAction = (args: never, context: any) => unknown
|
|
@ -1,35 +0,0 @@
|
|||||||
import { callOperation, makeOperationRoute } from '../operations'
|
|
||||||
import {
|
|
||||||
registerActionInProgress,
|
|
||||||
registerActionDone,
|
|
||||||
} from '../operations/resources'
|
|
||||||
|
|
||||||
export function createAction(relativeActionRoute, entitiesUsed) {
|
|
||||||
const actionRoute = makeOperationRoute(relativeActionRoute)
|
|
||||||
|
|
||||||
async function internalAction(args, specificOptimisticUpdateDefinitions) {
|
|
||||||
registerActionInProgress(specificOptimisticUpdateDefinitions)
|
|
||||||
try {
|
|
||||||
// The `return await` is not redundant here. If we removed the await, the
|
|
||||||
// `finally` block would execute before the action finishes, prematurely
|
|
||||||
// registering the action as done.
|
|
||||||
return await callOperation(actionRoute, args)
|
|
||||||
} finally {
|
|
||||||
await registerActionDone(entitiesUsed, specificOptimisticUpdateDefinitions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expose (and document) a restricted version of the API for our users,
|
|
||||||
// while also attaching the full "internal" API to the exposed action. By
|
|
||||||
// doing this, we can easily use the internal API of an action a users passes
|
|
||||||
// into our system (e.g., through the `useAction` hook) without needing a
|
|
||||||
// lookup table.
|
|
||||||
//
|
|
||||||
// While it does technically allow our users to access the interal API, it
|
|
||||||
// shouldn't be a problem in practice. Still, if it turns out to be a problem,
|
|
||||||
// we can always hide it using a Symbol.
|
|
||||||
const action = (args) => internalAction(args, [])
|
|
||||||
action.internal = internalAction
|
|
||||||
|
|
||||||
return action
|
|
||||||
}
|
|
@ -1,269 +0,0 @@
|
|||||||
import {
|
|
||||||
QueryClient,
|
|
||||||
QueryKey,
|
|
||||||
useMutation,
|
|
||||||
UseMutationOptions,
|
|
||||||
useQueryClient,
|
|
||||||
} from '@tanstack/react-query'
|
|
||||||
import { type Query } from '../queries';
|
|
||||||
|
|
||||||
export type Action<Input, Output> =
|
|
||||||
[Input] extends [never] ?
|
|
||||||
(args?: unknown) => Promise<Output> :
|
|
||||||
(args: Input) => Promise<Output>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An options object passed into the `useAction` hook and used to enhance the
|
|
||||||
* action with extra options.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export type ActionOptions<ActionInput> = {
|
|
||||||
optimisticUpdates: OptimisticUpdateDefinition<ActionInput, any>[]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A documented (public) way to define optimistic updates.
|
|
||||||
*/
|
|
||||||
export type OptimisticUpdateDefinition<ActionInput, CachedData> = {
|
|
||||||
getQuerySpecifier: GetQuerySpecifier<ActionInput, CachedData>
|
|
||||||
updateQuery: UpdateQuery<ActionInput, CachedData>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A function that takes an item and returns a Wasp Query specifier.
|
|
||||||
*/
|
|
||||||
export type GetQuerySpecifier<ActionInput, CachedData> = (item: ActionInput) => QuerySpecifier<unknown, CachedData>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A function that takes an item and the previous state of the cache, and returns
|
|
||||||
* the desired (new) state of the cache.
|
|
||||||
*/
|
|
||||||
export type UpdateQuery<ActionInput, CachedData> = (item: ActionInput, oldData: CachedData | undefined) => CachedData
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A public query specifier used for addressing Wasp queries. See our docs for details:
|
|
||||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
|
||||||
*/
|
|
||||||
export type QuerySpecifier<Input, Output> = [Query<Input, Output>, ...any[]]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A hook for adding extra behavior to a Wasp Action (e.g., optimistic updates).
|
|
||||||
*
|
|
||||||
* @param actionFn The Wasp Action you wish to enhance/decorate.
|
|
||||||
* @param actionOptions An options object for enhancing/decorating the given Action.
|
|
||||||
* @returns A decorated Action with added behavior but an unchanged API.
|
|
||||||
*/
|
|
||||||
export function useAction<Input = unknown, Output = unknown>(
|
|
||||||
actionFn: Action<Input, Output>,
|
|
||||||
actionOptions?: ActionOptions<Input>
|
|
||||||
): typeof actionFn {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
let mutationFn = actionFn
|
|
||||||
let options = {}
|
|
||||||
if (actionOptions?.optimisticUpdates) {
|
|
||||||
const optimisticUpdatesDefinitions = actionOptions.optimisticUpdates.map(translateToInternalDefinition)
|
|
||||||
mutationFn = makeOptimisticUpdateMutationFn(actionFn, optimisticUpdatesDefinitions)
|
|
||||||
options = makeRqOptimisticUpdateOptions(queryClient, optimisticUpdatesDefinitions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: We decided to hide React Query's extra mutation features (e.g.,
|
|
||||||
// isLoading, onSuccess and onError callbacks, synchronous mutate) and only
|
|
||||||
// expose a simple async function whose API matches the original Action.
|
|
||||||
// We did this to avoid cluttering the API with stuff we're not sure we need
|
|
||||||
// yet (e.g., isLoading), to postpone the action vs mutation dilemma, and to
|
|
||||||
// clearly separate our opinionated API from React Query's lower-level
|
|
||||||
// advanced API (which users can also use)
|
|
||||||
const mutation = useMutation(mutationFn, options)
|
|
||||||
return (args) => mutation.mutateAsync(args)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An internal (undocumented, private, desugared) way of defining optimistic updates.
|
|
||||||
*/
|
|
||||||
type InternalOptimisticUpdateDefinition<ActionInput, CachedData> = {
|
|
||||||
getQueryKey: (item: ActionInput) => QueryKey,
|
|
||||||
updateQuery: UpdateQuery<ActionInput, CachedData>;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An UpdateQuery function "instantiated" with a specific item. It only takes
|
|
||||||
* the current state of the cache and returns the desired (new) state of the
|
|
||||||
* cache.
|
|
||||||
*/
|
|
||||||
type SpecificUpdateQuery<CachedData> = (oldData: CachedData) => CachedData
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A specific, "instantiated" optimistic update definition which contains a
|
|
||||||
* fully-constructed query key and a specific update function.
|
|
||||||
*/
|
|
||||||
type SpecificOptimisticUpdateDefinition<Item> = {
|
|
||||||
queryKey: QueryKey;
|
|
||||||
updateQuery: SpecificUpdateQuery<Item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type InternalAction<Input, Output> = Action<Input, Output> & {
|
|
||||||
internal<CachedData extends unknown>(
|
|
||||||
item: Input,
|
|
||||||
optimisticUpdateDefinitions: SpecificOptimisticUpdateDefinition<CachedData>[]
|
|
||||||
): Promise<Output>
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translates/Desugars a public optimistic update definition object into a
|
|
||||||
* definition object our system uses internally.
|
|
||||||
*
|
|
||||||
* @param publicOptimisticUpdateDefinition An optimistic update definition
|
|
||||||
* object that's a part of the public API:
|
|
||||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
|
||||||
* @returns An internally-used optimistic update definition object.
|
|
||||||
*/
|
|
||||||
function translateToInternalDefinition<Item, CachedData>(
|
|
||||||
publicOptimisticUpdateDefinition: OptimisticUpdateDefinition<Item, CachedData>
|
|
||||||
): InternalOptimisticUpdateDefinition<Item, CachedData> {
|
|
||||||
const { getQuerySpecifier, updateQuery } = publicOptimisticUpdateDefinition
|
|
||||||
|
|
||||||
const definitionErrors = []
|
|
||||||
if (typeof getQuerySpecifier !== 'function') {
|
|
||||||
definitionErrors.push('`getQuerySpecifier` is not a function.')
|
|
||||||
}
|
|
||||||
if (typeof updateQuery !== 'function') {
|
|
||||||
definitionErrors.push('`updateQuery` is not a function.')
|
|
||||||
}
|
|
||||||
if (definitionErrors.length) {
|
|
||||||
throw new TypeError(`Invalid optimistic update definition: ${definitionErrors.join(', ')}.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getQueryKey: (item) => getRqQueryKeyFromSpecifier(getQuerySpecifier(item)),
|
|
||||||
updateQuery,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a function that performs an action while telling it about the
|
|
||||||
* optimistic updates it caused.
|
|
||||||
*
|
|
||||||
* @param actionFn The Wasp Action.
|
|
||||||
* @param optimisticUpdateDefinitions The optimisitc updates the action causes.
|
|
||||||
* @returns An decorated action which performs optimistic updates.
|
|
||||||
*/
|
|
||||||
function makeOptimisticUpdateMutationFn<Input, Output, CachedData>(
|
|
||||||
actionFn: Action<Input, Output>,
|
|
||||||
optimisticUpdateDefinitions: InternalOptimisticUpdateDefinition<Input, CachedData>[]
|
|
||||||
): typeof actionFn {
|
|
||||||
return function performActionWithOptimisticUpdates(item) {
|
|
||||||
const specificOptimisticUpdateDefinitions = optimisticUpdateDefinitions.map(
|
|
||||||
generalDefinition => getOptimisticUpdateDefinitionForSpecificItem(generalDefinition, item)
|
|
||||||
)
|
|
||||||
return (actionFn as InternalAction<Input, Output>).internal(item, specificOptimisticUpdateDefinitions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a ReactQuery query client and our internal definition of optimistic
|
|
||||||
* updates, this function constructs an object describing those same optimistic
|
|
||||||
* updates in a format we can pass into React Query's useMutation hook. In other
|
|
||||||
* words, it translates our optimistic updates definition into React Query's
|
|
||||||
* optimistic updates definition. Check their docs for details:
|
|
||||||
* https://tanstack.com/query/v4/docs/guides/optimistic-updates?from=reactQueryV3&original=https://react-query-v3.tanstack.com/guides/optimistic-updates
|
|
||||||
*
|
|
||||||
* @param queryClient The QueryClient instance used by React Query.
|
|
||||||
* @param optimisticUpdateDefinitions A list containing internal optimistic
|
|
||||||
* updates definition objects (i.e., a list where each object carries the
|
|
||||||
* instructions for performing particular optimistic update).
|
|
||||||
* @returns An object containing 'onMutate' and 'onError' functions
|
|
||||||
* corresponding to the given optimistic update definitions (check the docs
|
|
||||||
* linked above for details).
|
|
||||||
*/
|
|
||||||
function makeRqOptimisticUpdateOptions<ActionInput, CachedData>(
|
|
||||||
queryClient: QueryClient,
|
|
||||||
optimisticUpdateDefinitions: InternalOptimisticUpdateDefinition<ActionInput, CachedData>[]
|
|
||||||
): Pick<UseMutationOptions, "onMutate" | "onError"> {
|
|
||||||
async function onMutate(item) {
|
|
||||||
const specificOptimisticUpdateDefinitions = optimisticUpdateDefinitions.map(
|
|
||||||
generalDefinition => getOptimisticUpdateDefinitionForSpecificItem(generalDefinition, item)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cancel any outgoing refetches (so they don't overwrite our optimistic update).
|
|
||||||
// Theoretically, we can be a bit faster. Instead of awaiting the
|
|
||||||
// cancellation of all queries, we could cancel and update them in parallel.
|
|
||||||
// However, awaiting cancellation hasn't yet proven to be a performance bottleneck.
|
|
||||||
await Promise.all(specificOptimisticUpdateDefinitions.map(
|
|
||||||
({ queryKey }) => queryClient.cancelQueries(queryKey)
|
|
||||||
))
|
|
||||||
|
|
||||||
// We're using a Map to correctly serialize query keys that contain objects.
|
|
||||||
const previousData = new Map()
|
|
||||||
specificOptimisticUpdateDefinitions.forEach(({ queryKey, updateQuery }) => {
|
|
||||||
// Snapshot the currently cached value.
|
|
||||||
const previousDataForQuery: CachedData = queryClient.getQueryData(queryKey)
|
|
||||||
|
|
||||||
// Attempt to optimistically update the cache using the new value.
|
|
||||||
try {
|
|
||||||
queryClient.setQueryData(queryKey, updateQuery)
|
|
||||||
} catch (e) {
|
|
||||||
console.error("The `updateQuery` function threw an exception, skipping optimistic update:")
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remember the snapshotted value to restore in case of an error.
|
|
||||||
previousData.set(queryKey, previousDataForQuery)
|
|
||||||
})
|
|
||||||
|
|
||||||
return { previousData }
|
|
||||||
}
|
|
||||||
|
|
||||||
function onError(_err, _item, context) {
|
|
||||||
// All we do in case of an error is roll back all optimistic updates. We ensure
|
|
||||||
// not to do anything else because React Query rethrows the error. This allows
|
|
||||||
// the programmer to handle the error as they usually would (i.e., we want the
|
|
||||||
// error handling to work as it would if the programmer wasn't using optimistic
|
|
||||||
// updates).
|
|
||||||
context.previousData.forEach(async (data, queryKey) => {
|
|
||||||
await queryClient.cancelQueries(queryKey)
|
|
||||||
queryClient.setQueryData(queryKey, data)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
onMutate,
|
|
||||||
onError,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs the definition for optimistically updating a specific item. It
|
|
||||||
* uses a closure over the updated item to construct an item-specific query key
|
|
||||||
* (e.g., useful when the query key depends on an ID).
|
|
||||||
*
|
|
||||||
* @param optimisticUpdateDefinition The general, "uninstantiated" optimistic
|
|
||||||
* update definition with a function for constructing the query key.
|
|
||||||
* @param item The item triggering the Action/optimistic update (i.e., the
|
|
||||||
* argument passed to the Action).
|
|
||||||
* @returns A specific optimistic update definition which corresponds to the
|
|
||||||
* provided definition and closes over the provided item.
|
|
||||||
*/
|
|
||||||
function getOptimisticUpdateDefinitionForSpecificItem<ActionInput, CachedData>(
|
|
||||||
optimisticUpdateDefinition: InternalOptimisticUpdateDefinition<ActionInput, CachedData>,
|
|
||||||
item: ActionInput
|
|
||||||
): SpecificOptimisticUpdateDefinition<CachedData> {
|
|
||||||
const { getQueryKey, updateQuery } = optimisticUpdateDefinition
|
|
||||||
return {
|
|
||||||
queryKey: getQueryKey(item),
|
|
||||||
updateQuery: (old) => updateQuery(item, old)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translates a Wasp query specifier to a query cache key used by React Query.
|
|
||||||
*
|
|
||||||
* @param querySpecifier A query specifier that's a part of the public API:
|
|
||||||
* https://wasp-lang.dev/docs/data-model/operations/actions#the-useaction-hook-and-optimistic-updates
|
|
||||||
* @returns A cache key React Query internally uses for addressing queries.
|
|
||||||
*/
|
|
||||||
function getRqQueryKeyFromSpecifier(querySpecifier: QuerySpecifier<unknown, unknown>): QueryKey {
|
|
||||||
const [queryFn, ...otherKeys] = querySpecifier
|
|
||||||
return [...(queryFn as any).queryCacheKey, ...otherKeys]
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
import axios, { type AxiosError } from 'axios'
|
|
||||||
|
|
||||||
import config from './config'
|
|
||||||
import { storage } from './storage'
|
|
||||||
import { apiEventsEmitter } from './api/events'
|
|
||||||
|
|
||||||
const api = axios.create({
|
|
||||||
baseURL: config.apiUrl,
|
|
||||||
})
|
|
||||||
|
|
||||||
const WASP_APP_AUTH_SESSION_ID_NAME = 'sessionId'
|
|
||||||
|
|
||||||
let waspAppAuthSessionId = storage.get(WASP_APP_AUTH_SESSION_ID_NAME) as string | undefined
|
|
||||||
|
|
||||||
export function setSessionId(sessionId: string): void {
|
|
||||||
waspAppAuthSessionId = sessionId
|
|
||||||
storage.set(WASP_APP_AUTH_SESSION_ID_NAME, sessionId)
|
|
||||||
apiEventsEmitter.emit('sessionId.set')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSessionId(): string | undefined {
|
|
||||||
return waspAppAuthSessionId
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearSessionId(): void {
|
|
||||||
waspAppAuthSessionId = undefined
|
|
||||||
storage.remove(WASP_APP_AUTH_SESSION_ID_NAME)
|
|
||||||
apiEventsEmitter.emit('sessionId.clear')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeLocalUserData(): void {
|
|
||||||
waspAppAuthSessionId = undefined
|
|
||||||
storage.clear()
|
|
||||||
apiEventsEmitter.emit('sessionId.clear')
|
|
||||||
}
|
|
||||||
|
|
||||||
api.interceptors.request.use((request) => {
|
|
||||||
const sessionId = getSessionId()
|
|
||||||
if (sessionId) {
|
|
||||||
request.headers['Authorization'] = `Bearer ${sessionId}`
|
|
||||||
}
|
|
||||||
return request
|
|
||||||
})
|
|
||||||
|
|
||||||
api.interceptors.response.use(undefined, (error) => {
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
clearSessionId()
|
|
||||||
}
|
|
||||||
return Promise.reject(error)
|
|
||||||
})
|
|
||||||
|
|
||||||
// This handler will run on other tabs (not the active one calling API functions),
|
|
||||||
// and will ensure they know about auth session ID changes.
|
|
||||||
// Ref: https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
|
|
||||||
// "Note: This won't work on the same page that is making the changes — it is really a way
|
|
||||||
// for other pages on the domain using the storage to sync any changes that are made."
|
|
||||||
window.addEventListener('storage', (event) => {
|
|
||||||
if (event.key === storage.getPrefixedKey(WASP_APP_AUTH_SESSION_ID_NAME)) {
|
|
||||||
if (!!event.newValue) {
|
|
||||||
waspAppAuthSessionId = event.newValue
|
|
||||||
apiEventsEmitter.emit('sessionId.set')
|
|
||||||
} else {
|
|
||||||
waspAppAuthSessionId = undefined
|
|
||||||
apiEventsEmitter.emit('sessionId.clear')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes an error returned by the app's API (as returned by axios), and transforms into a more
|
|
||||||
* standard format to be further used by the client. It is also assumed that given API
|
|
||||||
* error has been formatted as implemented by HttpError on the server.
|
|
||||||
*/
|
|
||||||
export function handleApiError(error: AxiosError<{ message?: string, data?: unknown }>): void {
|
|
||||||
if (error?.response) {
|
|
||||||
// If error came from HTTP response, we capture most informative message
|
|
||||||
// and also add .statusCode information to it.
|
|
||||||
// If error had JSON response, we assume it is of format { message, data } and
|
|
||||||
// add that info to the error.
|
|
||||||
// TODO: We might want to use HttpError here instead of just Error, since
|
|
||||||
// HttpError is also used on server to throw errors like these.
|
|
||||||
// That would require copying HttpError code to web-app also and using it here.
|
|
||||||
const responseJson = error.response?.data
|
|
||||||
const responseStatusCode = error.response.status
|
|
||||||
throw new WaspHttpError(responseStatusCode, responseJson?.message ?? error.message, responseJson)
|
|
||||||
} else {
|
|
||||||
// If any other error, we just propagate it.
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WaspHttpError extends Error {
|
|
||||||
statusCode: number
|
|
||||||
|
|
||||||
data: unknown
|
|
||||||
|
|
||||||
constructor (statusCode: number, message: string, data: unknown) {
|
|
||||||
super(message)
|
|
||||||
this.statusCode = statusCode
|
|
||||||
this.data = data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default api
|
|
@ -1,95 +0,0 @@
|
|||||||
import { styled } from '../../../stitches.config'
|
|
||||||
|
|
||||||
export const Form = styled('form', {
|
|
||||||
marginTop: '1.5rem',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const FormItemGroup = styled('div', {
|
|
||||||
'& + div': {
|
|
||||||
marginTop: '1.5rem',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FormInput = styled('input', commonInputStyles)
|
|
||||||
|
|
||||||
export const FormTextarea = styled('textarea', commonInputStyles)
|
|
||||||
|
|
||||||
export const FormError = styled('div', {
|
|
||||||
display: 'block',
|
|
||||||
fontSize: '$sm',
|
|
||||||
fontWeight: '500',
|
|
||||||
color: '$formErrorText',
|
|
||||||
marginTop: '0.5rem',
|
|
||||||
})
|
|
||||||
|
|
||||||
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',
|
|
||||||
})
|
|
@ -1,18 +0,0 @@
|
|||||||
import { styled } from '../../../stitches.config'
|
|
||||||
|
|
||||||
export const Message = styled('div', {
|
|
||||||
padding: '0.5rem 0.75rem',
|
|
||||||
borderRadius: '0.375rem',
|
|
||||||
marginTop: '1rem',
|
|
||||||
background: '$gray400',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const MessageError = styled(Message, {
|
|
||||||
background: '$errorBackground',
|
|
||||||
color: '$errorText',
|
|
||||||
})
|
|
||||||
|
|
||||||
export const MessageSuccess = styled(Message, {
|
|
||||||
background: '$successBackground',
|
|
||||||
color: '$successText',
|
|
||||||
})
|
|
@ -1,14 +0,0 @@
|
|||||||
import { setSessionId } from '../../api'
|
|
||||||
import { invalidateAndRemoveQueries } from '../../operations/resources'
|
|
||||||
|
|
||||||
export async function initSession(sessionId: string): Promise<void> {
|
|
||||||
setSessionId(sessionId)
|
|
||||||
// We need to invalidate queries after login in order to get the correct user
|
|
||||||
// data in the React components (using `useAuth`).
|
|
||||||
// Redirects after login won't work properly without this.
|
|
||||||
|
|
||||||
// TODO(filip): We are currently removing all the queries, but we should
|
|
||||||
// remove only non-public, user-dependent queries - public queries are
|
|
||||||
// expected not to change in respect to the currently logged in user.
|
|
||||||
await invalidateAndRemoveQueries()
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import api, { removeLocalUserData } from '../api'
|
|
||||||
import { invalidateAndRemoveQueries } from '../operations/resources'
|
|
||||||
|
|
||||||
export default async function logout(): Promise<void> {
|
|
||||||
try {
|
|
||||||
await api.post('/auth/logout')
|
|
||||||
} finally {
|
|
||||||
// Even if the logout request fails, we still want to remove the local user data
|
|
||||||
// in case the logout failed because of a network error and the user walked away
|
|
||||||
// from the computer.
|
|
||||||
removeLocalUserData()
|
|
||||||
|
|
||||||
// TODO(filip): We are currently invalidating and removing all the queries, but
|
|
||||||
// we should remove only the non-public, user-dependent ones.
|
|
||||||
await invalidateAndRemoveQueries()
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,9 +2,9 @@
|
|||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
|
|
||||||
import config from '../../config.js'
|
import config from 'wasp/core/config'
|
||||||
import api from '../../api'
|
import api from 'wasp/api'
|
||||||
import { initSession } from '../helpers/user'
|
import { initSession } from 'wasp/auth/helpers/user'
|
||||||
|
|
||||||
// After a user authenticates via an Oauth 2.0 provider, this is the page that
|
// After a user authenticates via an Oauth 2.0 provider, this is the page that
|
||||||
// the provider should redirect them to, while providing query string parameters
|
// the provider should redirect them to, while providing query string parameters
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import { Redirect } from 'react-router-dom'
|
import { Redirect } from 'react-router-dom'
|
||||||
import useAuth from '../useAuth'
|
import useAuth from 'wasp/auth/useAuth'
|
||||||
|
|
||||||
|
|
||||||
const createAuthRequiredPage = (Page) => {
|
const createAuthRequiredPage = (Page) => {
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
// todo(filip): turn into a proper import/path
|
|
||||||
export type { SanitizedUser as User, ProviderName, DeserializedAuthIdentity } from '../../../server/src/_types/'
|
|
@ -1,40 +0,0 @@
|
|||||||
{{={= =}=}}
|
|
||||||
import { deserialize as superjsonDeserialize } from 'superjson'
|
|
||||||
import { useQuery } from '../queries'
|
|
||||||
import api, { handleApiError } from '../api'
|
|
||||||
import { HttpMethod } from '../types'
|
|
||||||
import type { User } from './types'
|
|
||||||
import { addMetadataToQuery } from '../queries/core'
|
|
||||||
|
|
||||||
|
|
||||||
export const getMe = createUserGetter()
|
|
||||||
|
|
||||||
export default function useAuth(queryFnArgs?: unknown, config?: any) {
|
|
||||||
return useQuery(getMe, queryFnArgs, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
function createUserGetter() {
|
|
||||||
const getMeRelativePath = 'auth/me'
|
|
||||||
const getMeRoute = { method: HttpMethod.Get, path: `/${getMeRelativePath}` }
|
|
||||||
async function getMe(): Promise<User | null> {
|
|
||||||
try {
|
|
||||||
const response = await api.get(getMeRoute.path)
|
|
||||||
|
|
||||||
return superjsonDeserialize(response.data)
|
|
||||||
} catch (error) {
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
return null
|
|
||||||
} else {
|
|
||||||
handleApiError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addMetadataToQuery(getMe, {
|
|
||||||
relativeQueryPath: getMeRelativePath,
|
|
||||||
queryRoute: getMeRoute,
|
|
||||||
entitiesUsed: {=& entitiesGetMeDependsOn =},
|
|
||||||
})
|
|
||||||
|
|
||||||
return getMe
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
// We decided not to deduplicate these helper functions in the server and the client.
|
|
||||||
// We have them duplicated in this file and in data/Generator/templates/server/src/auth/user.ts
|
|
||||||
// If you are changing the logic here, make sure to change it there as well.
|
|
||||||
|
|
||||||
import type { User, ProviderName, DeserializedAuthIdentity } from './types'
|
|
||||||
|
|
||||||
export function getEmail(user: User): string | null {
|
|
||||||
return findUserIdentity(user, "email")?.providerUserId ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUsername(user: User): string | null {
|
|
||||||
return findUserIdentity(user, "username")?.providerUserId ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFirstProviderUserId(user?: User): string | null {
|
|
||||||
if (!user || !user.auth || !user.auth.identities || user.auth.identities.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.auth.identities[0].providerUserId ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findUserIdentity(user: User, providerName: ProviderName): DeserializedAuthIdentity | undefined {
|
|
||||||
return user.auth.identities.find(
|
|
||||||
(identity) => identity.providerName === providerName
|
|
||||||
);
|
|
||||||
}
|
|
@ -14,7 +14,7 @@ import {
|
|||||||
{=/ setupFn.isDefined =}
|
{=/ setupFn.isDefined =}
|
||||||
|
|
||||||
{=# areWebSocketsUsed =}
|
{=# areWebSocketsUsed =}
|
||||||
import { WebSocketProvider } from './webSocket/WebSocketProvider'
|
import { WebSocketProvider } from 'wasp/webSocket/WebSocketProvider'
|
||||||
{=/ areWebSocketsUsed =}
|
{=/ areWebSocketsUsed =}
|
||||||
|
|
||||||
startApp()
|
startApp()
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
import api, { handleApiError } from '../api'
|
|
||||||
import { HttpMethod } from '../types'
|
|
||||||
import {
|
|
||||||
serialize as superjsonSerialize,
|
|
||||||
deserialize as superjsonDeserialize,
|
|
||||||
} from 'superjson'
|
|
||||||
|
|
||||||
export type OperationRoute = { method: HttpMethod, path: string }
|
|
||||||
|
|
||||||
export async function callOperation(operationRoute: OperationRoute & { method: HttpMethod.Post }, args: any) {
|
|
||||||
try {
|
|
||||||
const superjsonArgs = superjsonSerialize(args)
|
|
||||||
const response = await api.post(operationRoute.path, superjsonArgs)
|
|
||||||
return superjsonDeserialize(response.data)
|
|
||||||
} catch (error) {
|
|
||||||
handleApiError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeOperationRoute(relativeOperationRoute: string): OperationRoute {
|
|
||||||
return { method: HttpMethod.Post, path: `/${relativeOperationRoute}` }
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
import { queryClientInitialized } from '../queryClient'
|
|
||||||
import { makeUpdateHandlersMap } from './updateHandlersMap'
|
|
||||||
import { hashQueryKey } from '@tanstack/react-query'
|
|
||||||
|
|
||||||
// Map where key is resource name and value is Set
|
|
||||||
// containing query ids of all the queries that use
|
|
||||||
// that resource.
|
|
||||||
const resourceToQueryCacheKeys = new Map()
|
|
||||||
|
|
||||||
const updateHandlers = makeUpdateHandlersMap(hashQueryKey)
|
|
||||||
/**
|
|
||||||
* Remembers that specified query is using specified resources.
|
|
||||||
* If called multiple times for same query, resources are added, not reset.
|
|
||||||
* @param {string[]} queryCacheKey - Unique key under used to identify query in the cache.
|
|
||||||
* @param {string[]} resources - Names of resources that query is using.
|
|
||||||
*/
|
|
||||||
export function addResourcesUsedByQuery(queryCacheKey, resources) {
|
|
||||||
for (const resource of resources) {
|
|
||||||
let cacheKeys = resourceToQueryCacheKeys.get(resource)
|
|
||||||
if (!cacheKeys) {
|
|
||||||
cacheKeys = new Set()
|
|
||||||
resourceToQueryCacheKeys.set(resource, cacheKeys)
|
|
||||||
}
|
|
||||||
cacheKeys.add(queryCacheKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerActionInProgress(optimisticUpdateTuples) {
|
|
||||||
optimisticUpdateTuples.forEach(
|
|
||||||
({ queryKey, updateQuery }) => updateHandlers.add(queryKey, updateQuery)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function registerActionDone(resources, optimisticUpdateTuples) {
|
|
||||||
optimisticUpdateTuples.forEach(({ queryKey }) => updateHandlers.remove(queryKey))
|
|
||||||
await invalidateQueriesUsing(resources)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getActiveOptimisticUpdates(queryKey) {
|
|
||||||
return updateHandlers.getUpdateHandlers(queryKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function invalidateAndRemoveQueries() {
|
|
||||||
const queryClient = await queryClientInitialized
|
|
||||||
// If we don't reset the queries before removing them, Wasp will stay on
|
|
||||||
// the same page. The user would have to manually refresh the page to "finish"
|
|
||||||
// logging out.
|
|
||||||
// When a query is removed, the `Observer` is removed as well, and the components
|
|
||||||
// that are using the query are not re-rendered. This is why we need to reset
|
|
||||||
// the queries, so that the `Observer` is re-created and the components are re-rendered.
|
|
||||||
// For more details: https://github.com/wasp-lang/wasp/pull/1014/files#r1111862125
|
|
||||||
queryClient.resetQueries()
|
|
||||||
// If we don't remove the queries after invalidating them, the old query data
|
|
||||||
// remains in the cache, casuing a potential privacy issue.
|
|
||||||
queryClient.removeQueries()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidates all queries that are using specified resources.
|
|
||||||
* @param {string[]} resources - Names of resources.
|
|
||||||
*/
|
|
||||||
async function invalidateQueriesUsing(resources) {
|
|
||||||
const queryClient = await queryClientInitialized
|
|
||||||
|
|
||||||
const queryCacheKeysToInvalidate = getQueriesUsingResources(resources)
|
|
||||||
queryCacheKeysToInvalidate.forEach(
|
|
||||||
queryCacheKey => queryClient.invalidateQueries(queryCacheKey)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} resource - Resource name.
|
|
||||||
* @returns {string[]} Array of "query cache keys" of queries that use specified resource.
|
|
||||||
*/
|
|
||||||
function getQueriesUsingResource(resource) {
|
|
||||||
return Array.from(resourceToQueryCacheKeys.get(resource) || [])
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQueriesUsingResources(resources) {
|
|
||||||
return Array.from(new Set(resources.flatMap(getQueriesUsingResource)))
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
{{={= =}=}}
|
|
||||||
import { createQuery } from './core'
|
|
||||||
{=& operationTypeImportStmt =}
|
|
||||||
|
|
||||||
|
|
||||||
const query = createQuery<{= operationTypeName =}>(
|
|
||||||
'{= queryRoute =}',
|
|
||||||
{=& entitiesArray =},
|
|
||||||
)
|
|
||||||
|
|
||||||
export default query
|
|
@ -1,23 +0,0 @@
|
|||||||
import { type Query } from '.'
|
|
||||||
import { Route } from '../types';
|
|
||||||
import type { Expand, _Awaited, _ReturnType } from '../universal/types'
|
|
||||||
|
|
||||||
export function createQuery<BackendQuery extends GenericBackendQuery>(
|
|
||||||
queryRoute: string,
|
|
||||||
entitiesUsed: any[]
|
|
||||||
): QueryFor<BackendQuery>
|
|
||||||
|
|
||||||
export function addMetadataToQuery(
|
|
||||||
query: (...args: any[]) => Promise<unknown>,
|
|
||||||
metadata: {
|
|
||||||
relativeQueryPath: string;
|
|
||||||
queryRoute: Route;
|
|
||||||
entitiesUsed: string[];
|
|
||||||
},
|
|
||||||
): void
|
|
||||||
|
|
||||||
type QueryFor<BackendQuery extends GenericBackendQuery> = Expand<
|
|
||||||
Query<Parameters<BackendQuery>[0], _Awaited<_ReturnType<BackendQuery>>>
|
|
||||||
>
|
|
||||||
|
|
||||||
type GenericBackendQuery = (args: never, context: any) => unknown
|
|
@ -1,27 +0,0 @@
|
|||||||
import { callOperation, makeOperationRoute } from '../operations'
|
|
||||||
import {
|
|
||||||
addResourcesUsedByQuery,
|
|
||||||
getActiveOptimisticUpdates,
|
|
||||||
} from '../operations/resources'
|
|
||||||
|
|
||||||
export function createQuery(relativeQueryPath, entitiesUsed) {
|
|
||||||
const queryRoute = makeOperationRoute(relativeQueryPath)
|
|
||||||
|
|
||||||
async function query(queryKey, queryArgs) {
|
|
||||||
const serverResult = await callOperation(queryRoute, queryArgs)
|
|
||||||
return getActiveOptimisticUpdates(queryKey).reduce(
|
|
||||||
(result, update) => update(result),
|
|
||||||
serverResult,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
addMetadataToQuery(query, { relativeQueryPath, queryRoute, entitiesUsed })
|
|
||||||
|
|
||||||
return query
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addMetadataToQuery(query, { relativeQueryPath, queryRoute, entitiesUsed }) {
|
|
||||||
query.queryCacheKey = [relativeQueryPath]
|
|
||||||
query.route = queryRoute
|
|
||||||
addResourcesUsedByQuery(query.queryCacheKey, entitiesUsed)
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
import { UseQueryResult } from "@tanstack/react-query";
|
|
||||||
|
|
||||||
export type Query<Input, Output> = {
|
|
||||||
(queryCacheKey: string[], args: Input): Promise<Output>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useQuery<Input, Output>(
|
|
||||||
queryFn: Query<Input, Output>,
|
|
||||||
queryFnArgs?: Input, options?: any
|
|
||||||
): UseQueryResult<Output, Error>
|
|
@ -1,18 +0,0 @@
|
|||||||
import { useQuery as rqUseQuery } from '@tanstack/react-query'
|
|
||||||
export { configureQueryClient } from '../queryClient'
|
|
||||||
|
|
||||||
export function useQuery(queryFn, queryFnArgs, options) {
|
|
||||||
if (typeof queryFn !== 'function') {
|
|
||||||
throw new TypeError('useQuery requires queryFn to be a function.')
|
|
||||||
}
|
|
||||||
if (!queryFn.queryCacheKey) {
|
|
||||||
throw new TypeError('queryFn needs to have queryCacheKey property defined.')
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryKey = queryFnArgs !== undefined ? [...queryFn.queryCacheKey, queryFnArgs] : queryFn.queryCacheKey
|
|
||||||
return rqUseQuery({
|
|
||||||
queryKey,
|
|
||||||
queryFn: () => queryFn(queryKey, queryFnArgs),
|
|
||||||
...options
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,18 +1,12 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Route, Switch, BrowserRouter as Router } from 'react-router-dom'
|
import { Route, Switch, BrowserRouter as Router } from 'react-router-dom'
|
||||||
import { interpolatePath } from './router/linkHelpers'
|
|
||||||
import type {
|
|
||||||
RouteDefinitionsToRoutes,
|
|
||||||
OptionalRouteOptions,
|
|
||||||
ParamValue,
|
|
||||||
} from './router/types'
|
|
||||||
{=# rootComponent.isDefined =}
|
{=# rootComponent.isDefined =}
|
||||||
{=& rootComponent.importStatement =}
|
{=& rootComponent.importStatement =}
|
||||||
{=/ rootComponent.isDefined =}
|
{=/ rootComponent.isDefined =}
|
||||||
|
|
||||||
{=# isAuthEnabled =}
|
{=# isAuthEnabled =}
|
||||||
import createAuthRequiredPage from "wasp/auth/pages/createAuthRequiredPage"
|
import createAuthRequiredPage from "./auth/pages/createAuthRequiredPage"
|
||||||
{=/ isAuthEnabled =}
|
{=/ isAuthEnabled =}
|
||||||
|
|
||||||
{=# pagesToImport =}
|
{=# pagesToImport =}
|
||||||
@ -23,29 +17,14 @@ import createAuthRequiredPage from "wasp/auth/pages/createAuthRequiredPage"
|
|||||||
import OAuthCodeExchange from "./auth/pages/OAuthCodeExchange"
|
import OAuthCodeExchange from "./auth/pages/OAuthCodeExchange"
|
||||||
{=/ isExternalAuthEnabled =}
|
{=/ isExternalAuthEnabled =}
|
||||||
|
|
||||||
export const routes = {
|
import { routes } from 'wasp/router'
|
||||||
|
|
||||||
|
export const routeNameToRouteComponent = {
|
||||||
{=# routes =}
|
{=# routes =}
|
||||||
{= name =}: {
|
{= name =}: {= targetComponent =},
|
||||||
to: "{= urlPath =}",
|
|
||||||
component: {= targetComponent =},
|
|
||||||
{=# hasUrlParams =}
|
|
||||||
build: (
|
|
||||||
options: {
|
|
||||||
params: {{=# urlParams =}{= name =}{=# isOptional =}?{=/ isOptional =}: ParamValue;{=/ urlParams =}}
|
|
||||||
} & OptionalRouteOptions,
|
|
||||||
) => interpolatePath("{= urlPath =}", options.params, options.search, options.hash),
|
|
||||||
{=/ hasUrlParams =}
|
|
||||||
{=^ hasUrlParams =}
|
|
||||||
build: (
|
|
||||||
options?: OptionalRouteOptions,
|
|
||||||
) => interpolatePath("{= urlPath =}", undefined, options.search, options.hash),
|
|
||||||
{=/ hasUrlParams =}
|
|
||||||
},
|
|
||||||
{=/ routes =}
|
{=/ routes =}
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type Routes = RouteDefinitionsToRoutes<typeof routes>
|
|
||||||
|
|
||||||
const router = (
|
const router = (
|
||||||
<Router basename="{= baseDir =}">
|
<Router basename="{= baseDir =}">
|
||||||
{=# rootComponent.isDefined =}
|
{=# rootComponent.isDefined =}
|
||||||
@ -57,7 +36,7 @@ const router = (
|
|||||||
exact
|
exact
|
||||||
key={routeKey}
|
key={routeKey}
|
||||||
path={route.to}
|
path={route.to}
|
||||||
component={route.component}
|
component={routeNameToRouteComponent[routeKey]}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{=# isExternalAuthEnabled =}
|
{=# isExternalAuthEnabled =}
|
||||||
@ -77,5 +56,3 @@ const router = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
||||||
export { Link } from './router/Link'
|
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import matchers from '@testing-library/jest-dom/matchers'
|
import { afterEach } from 'vitest'
|
||||||
import { expect } from 'vitest'
|
import { cleanup } from '@testing-library/react'
|
||||||
|
import '@testing-library/jest-dom/vitest'
|
||||||
|
|
||||||
expect.extend(matchers)
|
// runs a clean after each test case (e.g. clearing jsdom)
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
})
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
// NOTE: This is enough to cover Operations and our APIs (src/Wasp/AppSpec/Api.hs).
|
|
||||||
export enum HttpMethod {
|
|
||||||
Get = 'GET',
|
|
||||||
Post = 'POST',
|
|
||||||
Put = 'PUT',
|
|
||||||
Delete = 'DELETE',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Route = { method: HttpMethod; path: string }
|
|
@ -6,7 +6,7 @@
|
|||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
// Allow importing pages with the .tsx extension.
|
// Allow importing pages with the .tsx extension.
|
||||||
"allowImportingTsExtensions": true
|
"allowImportingTsExtensions": true,
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
|
@ -2,8 +2,11 @@
|
|||||||
/// <reference types="vitest" />
|
/// <reference types="vitest" />
|
||||||
import { mergeConfig } from "vite";
|
import { mergeConfig } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { defaultExclude } from "vitest/config"
|
||||||
|
|
||||||
{=# customViteConfig.isDefined =}
|
{=# customViteConfig.isDefined =}
|
||||||
|
// Ignoring the TS error because we are importing a file outside of TS root dir.
|
||||||
|
// @ts-ignore
|
||||||
{=& customViteConfig.importStatement =}
|
{=& customViteConfig.importStatement =}
|
||||||
const _waspUserProvidedConfig = {=& customViteConfig.importIdentifier =}
|
const _waspUserProvidedConfig = {=& customViteConfig.importIdentifier =}
|
||||||
{=/ customViteConfig.isDefined =}
|
{=/ customViteConfig.isDefined =}
|
||||||
@ -27,12 +30,16 @@ const defaultViteConfig = {
|
|||||||
outDir: "build",
|
outDir: "build",
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
|
globals: true,
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
setupFiles: ["./src/test/vitest/setup.ts"],
|
// vitest is running from the root of the project, so we need
|
||||||
|
// to specify the path to the setup file relative to the root.
|
||||||
|
setupFiles: {=& vitest.setupFilesArray =},
|
||||||
|
exclude: [
|
||||||
|
...defaultExclude,
|
||||||
|
"{= vitest.excludeWaspArtefactsPattern =}",
|
||||||
|
]
|
||||||
},
|
},
|
||||||
// resolve: {
|
|
||||||
// dedupe: ["react", "react-dom"],
|
|
||||||
// },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
|
@ -2,7 +2,7 @@ import axios, { type AxiosError } from 'axios'
|
|||||||
|
|
||||||
import config from 'wasp/core/config'
|
import config from 'wasp/core/config'
|
||||||
import { storage } from 'wasp/core/storage'
|
import { storage } from 'wasp/core/storage'
|
||||||
import { apiEventsEmitter } from 'wasp/api/events'
|
import { apiEventsEmitter } from './events.js'
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: config.apiUrl,
|
baseURL: config.apiUrl,
|
@ -1,5 +1,5 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import api, { handleApiError } from '../../../api';
|
import api, { handleApiError } from 'wasp/api';
|
||||||
import { initSession } from '../../helpers/user';
|
import { initSession } from '../../helpers/user';
|
||||||
|
|
||||||
export async function login(data: { email: string; password: string }): Promise<void> {
|
export async function login(data: { email: string; password: string }): Promise<void> {
|
@ -1,5 +1,5 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import api, { handleApiError } from '../../../api';
|
import api, { handleApiError } from 'wasp/api';
|
||||||
|
|
||||||
export async function requestPasswordReset(data: { email: string; }): Promise<{ success: boolean }> {
|
export async function requestPasswordReset(data: { email: string; }): Promise<{ success: boolean }> {
|
||||||
try {
|
try {
|
@ -1,5 +1,5 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import api, { handleApiError } from '../../../api';
|
import api, { handleApiError } from 'wasp/api';
|
||||||
|
|
||||||
export async function signup(data: { email: string; password: string }): Promise<{ success: boolean }> {
|
export async function signup(data: { email: string; password: string }): Promise<{ success: boolean }> {
|
||||||
try {
|
try {
|
@ -1,5 +1,5 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import api, { handleApiError } from '../../../api'
|
import api, { handleApiError } from 'wasp/api'
|
||||||
|
|
||||||
export async function verifyEmail(data: {
|
export async function verifyEmail(data: {
|
||||||
token: string
|
token: string
|
@ -1,7 +1,7 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import { useState, createContext } from 'react'
|
import { useState, createContext } from 'react'
|
||||||
import { createTheme } from '@stitches/react'
|
import { createTheme } from '@stitches/react'
|
||||||
import { styled } from '../../stitches.config'
|
import { styled } from 'wasp/core/stitches.config'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type State,
|
type State,
|
@ -1,8 +1,8 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import { useContext } from 'react'
|
import { useContext } from 'react'
|
||||||
import { useForm, UseFormReturn } from 'react-hook-form'
|
import { useForm, UseFormReturn } from 'react-hook-form'
|
||||||
import { styled } from '../../../../stitches.config'
|
import { styled } from 'wasp/core/stitches.config'
|
||||||
import config from '../../../../config'
|
import config from 'wasp/core/config'
|
||||||
|
|
||||||
import { AuthContext } from '../../Auth'
|
import { AuthContext } from '../../Auth'
|
||||||
import {
|
import {
|
@ -1,4 +1,4 @@
|
|||||||
import { styled } from '../../../../stitches.config'
|
import { styled } from 'wasp/core/stitches.config'
|
||||||
|
|
||||||
export const SocialButton = styled('a', {
|
export const SocialButton = styled('a', {
|
||||||
display: 'flex',
|
display: 'flex',
|
@ -1,6 +1,6 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
|
|
||||||
import config from '../../config.js'
|
import config from 'wasp/core/config'
|
||||||
import { SocialButton } from '../forms/internal/social/SocialButton'
|
import { SocialButton } from '../forms/internal/social/SocialButton'
|
||||||
import * as SocialIcons from '../forms/internal/social/SocialIcons'
|
import * as SocialIcons from '../forms/internal/social/SocialIcons'
|
||||||
|
|
@ -1,7 +1,7 @@
|
|||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import util from 'util'
|
import util from 'util'
|
||||||
|
|
||||||
import config from '../config.js'
|
import config from 'wasp/server/config'
|
||||||
|
|
||||||
const jwtSign = util.promisify(jwt.sign)
|
const jwtSign = util.promisify(jwt.sign)
|
||||||
const jwtVerify = util.promisify(jwt.verify)
|
const jwtVerify = util.promisify(jwt.verify)
|
@ -1,5 +1,5 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import api, { handleApiError } from '../api'
|
import api, { handleApiError } from 'wasp/api'
|
||||||
import { initSession } from './helpers/user'
|
import { initSession } from './helpers/user'
|
||||||
|
|
||||||
export default async function login(username: string, password: string): Promise<void> {
|
export default async function login(username: string, password: string): Promise<void> {
|
@ -1,9 +1,8 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import { Lucia } from "lucia";
|
import { Lucia } from "lucia";
|
||||||
import { PrismaAdapter } from "@lucia-auth/adapter-prisma";
|
import { PrismaAdapter } from "@lucia-auth/adapter-prisma";
|
||||||
import prisma from '../dbClient.js'
|
import prisma from 'wasp/server/dbClient'
|
||||||
import config from '../config.js'
|
import { type {= userEntityUpper =} } from "wasp/entities"
|
||||||
import { type {= userEntityUpper =} } from "../entities/index.js"
|
|
||||||
|
|
||||||
const prismaAdapter = new PrismaAdapter(
|
const prismaAdapter = new PrismaAdapter(
|
||||||
// Using `as any` here since Lucia's model types are not compatible with Prisma 4
|
// Using `as any` here since Lucia's model types are not compatible with Prisma 4
|
@ -1,7 +1,7 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import type { Router, Request } from 'express'
|
import type { Router, Request } from 'express'
|
||||||
import type { Prisma } from '@prisma/client'
|
import type { Prisma } from '@prisma/client'
|
||||||
import type { Expand } from '../../universal/types'
|
import type { Expand } from 'wasp/universal/types'
|
||||||
import type { ProviderName } from '../utils'
|
import type { ProviderName } from '../utils'
|
||||||
|
|
||||||
type UserEntityCreateInput = Prisma.{= userEntityUpper =}CreateInput
|
type UserEntityCreateInput = Prisma.{= userEntityUpper =}CreateInput
|
@ -1,8 +1,8 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import { Request as ExpressRequest } from "express";
|
import { Request as ExpressRequest } from "express";
|
||||||
|
|
||||||
import { type {= userEntityUpper =} } from "../entities/index.js"
|
import { type {= userEntityUpper =} } from "wasp/entities"
|
||||||
import { type SanitizedUser } from '../_types/index.js'
|
import { type SanitizedUser } from 'wasp/server/_types'
|
||||||
|
|
||||||
import { auth } from "./lucia.js";
|
import { auth } from "./lucia.js";
|
||||||
import type { Session } from "lucia";
|
import type { Session } from "lucia";
|
||||||
@ -11,7 +11,7 @@ import {
|
|||||||
deserializeAndSanitizeProviderData,
|
deserializeAndSanitizeProviderData,
|
||||||
} from "./utils.js";
|
} from "./utils.js";
|
||||||
|
|
||||||
import prisma from '../dbClient.js';
|
import prisma from 'wasp/server/dbClient';
|
||||||
|
|
||||||
// Creates a new session for the `authId` in the database
|
// Creates a new session for the `authId` in the database
|
||||||
export async function createSession(authId: string): Promise<Session> {
|
export async function createSession(authId: string): Promise<Session> {
|
@ -1,5 +1,5 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import api, { handleApiError } from '../api'
|
import api, { handleApiError } from 'wasp/api'
|
||||||
|
|
||||||
export default async function signup(userFields: { username: string; password: string }): Promise<void> {
|
export default async function signup(userFields: { username: string; password: string }): Promise<void> {
|
||||||
try {
|
try {
|
@ -1,8 +1,9 @@
|
|||||||
|
{{={= =}=}}
|
||||||
import { deserialize as superjsonDeserialize } from 'superjson'
|
import { deserialize as superjsonDeserialize } from 'superjson'
|
||||||
import { useQuery } from 'wasp/rpc'
|
import { useQuery } from 'wasp/rpc'
|
||||||
import api, { handleApiError } from 'wasp/api'
|
import api, { handleApiError } from 'wasp/api'
|
||||||
import { HttpMethod } from 'wasp/types'
|
import { HttpMethod } from 'wasp/types'
|
||||||
import type { User } from './types'
|
import type { User } from './types'
|
||||||
import { addMetadataToQuery } from 'wasp/rpc/queries'
|
import { addMetadataToQuery } from 'wasp/rpc/queries'
|
||||||
|
|
||||||
export const getMe = createUserGetter()
|
export const getMe = createUserGetter()
|
||||||
@ -31,7 +32,7 @@ function createUserGetter() {
|
|||||||
addMetadataToQuery(getMe, {
|
addMetadataToQuery(getMe, {
|
||||||
relativeQueryPath: getMeRelativePath,
|
relativeQueryPath: getMeRelativePath,
|
||||||
queryRoute: getMeRoute,
|
queryRoute: getMeRoute,
|
||||||
entitiesUsed: ['User'],
|
entitiesUsed: {=& entitiesGetMeDependsOn =},
|
||||||
})
|
})
|
||||||
|
|
||||||
return getMe
|
return getMe
|
@ -1,8 +1,4 @@
|
|||||||
// We decided not to deduplicate these helper functions in the server and the client.
|
import type { User, ProviderName, DeserializedAuthIdentity } from './types'
|
||||||
// We have them duplicated in this file and in data/Generator/templates/server/src/auth/user.ts
|
|
||||||
// If you are changing the logic here, make sure to change it there as well.
|
|
||||||
|
|
||||||
import type { User, ProviderName, DeserializedAuthIdentity } from './types'
|
|
||||||
|
|
||||||
export function getEmail(user: User): string | null {
|
export function getEmail(user: User): string | null {
|
||||||
return findUserIdentity(user, "email")?.providerUserId ?? null;
|
return findUserIdentity(user, "email")?.providerUserId ?? null;
|
@ -1,14 +1,15 @@
|
|||||||
|
{{={= =}=}}
|
||||||
import { hashPassword } from './password.js'
|
import { hashPassword } from './password.js'
|
||||||
import { verify } from './jwt.js'
|
import { verify } from './jwt.js'
|
||||||
import AuthError from '../core/AuthError.js'
|
import AuthError from 'wasp/core/AuthError'
|
||||||
import HttpError from '../core/HttpError.js'
|
import HttpError from 'wasp/core/HttpError'
|
||||||
import prisma from '../server/dbClient.js'
|
import prisma from 'wasp/server/dbClient'
|
||||||
import { sleep } from '../server/utils'
|
import { sleep } from 'wasp/server/utils'
|
||||||
import {
|
import {
|
||||||
type User,
|
type {= userEntityUpper =},
|
||||||
type Auth,
|
type {= authEntityUpper =},
|
||||||
type AuthIdentity,
|
type {= authIdentityEntityUpper =},
|
||||||
} from '../entities'
|
} from 'wasp/entities'
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
import { throwValidationError } from './validation.js'
|
import { throwValidationError } from './validation.js'
|
||||||
@ -46,13 +47,13 @@ export type ProviderName = keyof PossibleProviderData
|
|||||||
|
|
||||||
export const contextWithUserEntity = {
|
export const contextWithUserEntity = {
|
||||||
entities: {
|
entities: {
|
||||||
User: prisma.user
|
{= userEntityUpper =}: prisma.{= userEntityLower =}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authConfig = {
|
export const authConfig = {
|
||||||
failureRedirectPath: "/login",
|
failureRedirectPath: "{= failureRedirectPath =}",
|
||||||
successRedirectPath: "/",
|
successRedirectPath: "{= successRedirectPath =}",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -76,8 +77,8 @@ export function createProviderId(providerName: ProviderName, providerUserId: str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findAuthIdentity(providerId: ProviderId): Promise<AuthIdentity | null> {
|
export async function findAuthIdentity(providerId: ProviderId): Promise<{= authIdentityEntityUpper =} | null> {
|
||||||
return prisma.authIdentity.findUnique({
|
return prisma.{= authIdentityEntityLower =}.findUnique({
|
||||||
where: {
|
where: {
|
||||||
providerName_providerUserId: providerId,
|
providerName_providerUserId: providerId,
|
||||||
}
|
}
|
||||||
@ -96,7 +97,7 @@ export async function updateAuthIdentityProviderData<PN extends ProviderName>(
|
|||||||
providerId: ProviderId,
|
providerId: ProviderId,
|
||||||
existingProviderData: PossibleProviderData[PN],
|
existingProviderData: PossibleProviderData[PN],
|
||||||
providerDataUpdates: Partial<PossibleProviderData[PN]>,
|
providerDataUpdates: Partial<PossibleProviderData[PN]>,
|
||||||
): Promise<AuthIdentity> {
|
): Promise<{= authIdentityEntityUpper =}> {
|
||||||
// We are doing the sanitization here only on updates to avoid
|
// We are doing the sanitization here only on updates to avoid
|
||||||
// hashing the password multiple times.
|
// hashing the password multiple times.
|
||||||
const sanitizedProviderDataUpdates = await sanitizeProviderData(providerDataUpdates);
|
const sanitizedProviderDataUpdates = await sanitizeProviderData(providerDataUpdates);
|
||||||
@ -105,7 +106,7 @@ export async function updateAuthIdentityProviderData<PN extends ProviderName>(
|
|||||||
...sanitizedProviderDataUpdates,
|
...sanitizedProviderDataUpdates,
|
||||||
}
|
}
|
||||||
const serializedProviderData = await serializeProviderData<PN>(newProviderData);
|
const serializedProviderData = await serializeProviderData<PN>(newProviderData);
|
||||||
return prisma.authIdentity.update({
|
return prisma.{= authIdentityEntityLower =}.update({
|
||||||
where: {
|
where: {
|
||||||
providerName_providerUserId: providerId,
|
providerName_providerUserId: providerId,
|
||||||
},
|
},
|
||||||
@ -113,31 +114,31 @@ export async function updateAuthIdentityProviderData<PN extends ProviderName>(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindAuthWithUserResult = Auth & {
|
type FindAuthWithUserResult = {= authEntityUpper =} & {
|
||||||
user: User
|
{= userFieldOnAuthEntityName =}: {= userEntityUpper =}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findAuthWithUserBy(
|
export async function findAuthWithUserBy(
|
||||||
where: Prisma.AuthWhereInput
|
where: Prisma.{= authEntityUpper =}WhereInput
|
||||||
): Promise<FindAuthWithUserResult> {
|
): Promise<FindAuthWithUserResult> {
|
||||||
return prisma.auth.findFirst({ where, include: { user: true }});
|
return prisma.{= authEntityLower =}.findFirst({ where, include: { {= userFieldOnAuthEntityName =}: true }});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createUser(
|
export async function createUser(
|
||||||
providerId: ProviderId,
|
providerId: ProviderId,
|
||||||
serializedProviderData?: string,
|
serializedProviderData?: string,
|
||||||
userFields?: PossibleUserFields,
|
userFields?: PossibleUserFields,
|
||||||
): Promise<User & {
|
): Promise<{= userEntityUpper =} & {
|
||||||
auth: Auth
|
auth: {= authEntityUpper =}
|
||||||
}> {
|
}> {
|
||||||
return prisma.user.create({
|
return prisma.{= userEntityLower =}.create({
|
||||||
data: {
|
data: {
|
||||||
// Using any here to prevent type errors when userFields are not
|
// Using any here to prevent type errors when userFields are not
|
||||||
// defined. We want Prisma to throw an error in that case.
|
// defined. We want Prisma to throw an error in that case.
|
||||||
...(userFields ?? {} as any),
|
...(userFields ?? {} as any),
|
||||||
auth: {
|
{= authFieldOnUserEntityName =}: {
|
||||||
create: {
|
create: {
|
||||||
identities: {
|
{= identitiesFieldOnAuthEntityName =}: {
|
||||||
create: {
|
create: {
|
||||||
providerName: providerId.providerName,
|
providerName: providerId.providerName,
|
||||||
providerUserId: providerId.providerUserId,
|
providerUserId: providerId.providerUserId,
|
||||||
@ -150,13 +151,13 @@ export async function createUser(
|
|||||||
// We need to include the Auth entity here because we need `authId`
|
// We need to include the Auth entity here because we need `authId`
|
||||||
// to be able to create a session.
|
// to be able to create a session.
|
||||||
include: {
|
include: {
|
||||||
auth: true,
|
{= authFieldOnUserEntityName =}: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteUserByAuthId(authId: string): Promise<{ count: number }> {
|
export async function deleteUserByAuthId(authId: string): Promise<{ count: number }> {
|
||||||
return prisma.user.deleteMany({ where: { auth: {
|
return prisma.{= userEntityLower =}.deleteMany({ where: { auth: {
|
||||||
id: authId,
|
id: authId,
|
||||||
} } })
|
} } })
|
||||||
}
|
}
|
||||||
@ -213,7 +214,7 @@ export function rethrowPossibleAuthError(e: unknown): void {
|
|||||||
// Prisma code P2003 is for foreign key constraint failure
|
// Prisma code P2003 is for foreign key constraint failure
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2003') {
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2003') {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
console.info(`🐝 This error can happen if you have some relation on your User entity
|
console.info(`🐝 This error can happen if you have some relation on your {= userEntityUpper =} entity
|
||||||
but you didn't specify the "onDelete" behaviour to either "Cascade" or "SetNull".
|
but you didn't specify the "onDelete" behaviour to either "Cascade" or "SetNull".
|
||||||
Read more at: https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions`)
|
Read more at: https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions`)
|
||||||
throw new HttpError(500, 'Save failed', {
|
throw new HttpError(500, 'Save failed', {
|
@ -1,4 +1,4 @@
|
|||||||
import HttpError from 'wasp/core/HttpError'
|
import HttpError from 'wasp/core/HttpError';
|
||||||
|
|
||||||
export const PASSWORD_FIELD = 'password';
|
export const PASSWORD_FIELD = 'password';
|
||||||
const USERNAME_FIELD = 'username';
|
const USERNAME_FIELD = 'username';
|
@ -1,5 +1,7 @@
|
|||||||
class AuthError extends Error {
|
class AuthError extends Error {
|
||||||
constructor (message, data, ...params) {
|
public data: unknown
|
||||||
|
|
||||||
|
constructor (message: string, data?: unknown, ...params: unknown[]) {
|
||||||
super(message, ...params)
|
super(message, ...params)
|
||||||
|
|
||||||
if (Error.captureStackTrace) {
|
if (Error.captureStackTrace) {
|
@ -1,5 +1,8 @@
|
|||||||
class HttpError extends Error {
|
class HttpError extends Error {
|
||||||
constructor (statusCode, message, data, ...params) {
|
public statusCode: number
|
||||||
|
public data: unknown
|
||||||
|
|
||||||
|
constructor (statusCode: number, message?: string, data?: Record<string, unknown>, ...params: unknown[]) {
|
||||||
super(message, ...params)
|
super(message, ...params)
|
||||||
|
|
||||||
if (Error.captureStackTrace) {
|
if (Error.captureStackTrace) {
|
@ -1,7 +1,4 @@
|
|||||||
import { randomInt } from 'node:crypto'
|
import { handleRejection } from 'wasp/server/utils'
|
||||||
|
|
||||||
import prisma from '../server/dbClient.js'
|
|
||||||
import { handleRejection } from '../utils.js'
|
|
||||||
import { getSessionAndUserFromBearerToken } from 'wasp/auth/session'
|
import { getSessionAndUserFromBearerToken } from 'wasp/auth/session'
|
||||||
import { throwInvalidCredentialsError } from 'wasp/auth/utils'
|
import { throwInvalidCredentialsError } from 'wasp/auth/utils'
|
||||||
|
|
@ -1,8 +1,8 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import { createAction } from "../actions/core";
|
import { createAction } from "wasp/rpc/actions/core";
|
||||||
import { useAction } from "../actions";
|
import { useAction } from "wasp/rpc";
|
||||||
import { createQuery } from "../queries/core";
|
import { createQuery } from "wasp/rpc/queries/core";
|
||||||
import { useQuery } from "../queries";
|
import { useQuery } from "wasp/rpc";
|
||||||
import {
|
import {
|
||||||
{=# operations.Get =}
|
{=# operations.Get =}
|
||||||
GetQueryResolved,
|
GetQueryResolved,
|
||||||
@ -19,7 +19,7 @@ import {
|
|||||||
{=# operations.Delete =}
|
{=# operations.Delete =}
|
||||||
DeleteActionResolved,
|
DeleteActionResolved,
|
||||||
{=/ operations.Delete =}
|
{=/ operations.Delete =}
|
||||||
} from '../../../server/src/crud/{= name =}'
|
} from 'wasp/server/crud/{= name =}'
|
||||||
|
|
||||||
function createCrud() {
|
function createCrud() {
|
||||||
{=# operations.Get =}
|
{=# operations.Get =}
|
@ -1,5 +1,5 @@
|
|||||||
{{={= =}=}}
|
{{={= =}=}}
|
||||||
import { EmailFromField } from "./types";
|
import { EmailFromField } from "wasp/email/core/types";
|
||||||
|
|
||||||
// Formats an email address and an optional name into a string that can be used
|
// Formats an email address and an optional name into a string that can be used
|
||||||
// as the "from" field in an email.
|
// as the "from" field in an email.
|
@ -1,4 +1,4 @@
|
|||||||
import { DummyEmailProvider, EmailSender } from "../types.js";
|
import { DummyEmailProvider, EmailSender } from "wasp/email/core/types";
|
||||||
import { getDefaultFromField } from "../helpers.js";
|
import { getDefaultFromField } from "../helpers.js";
|
||||||
|
|
||||||
const yellowColor = "\x1b[33m%s\x1b[0m";
|
const yellowColor = "\x1b[33m%s\x1b[0m";
|
@ -1,6 +1,6 @@
|
|||||||
import { NodeMailgun } from "ts-mailgun";
|
import { NodeMailgun } from "ts-mailgun";
|
||||||
import { getDefaultFromField } from "../helpers.js";
|
import { getDefaultFromField } from "../helpers.js";
|
||||||
import type { MailgunEmailProvider, EmailSender } from "../types.js";
|
import type { MailgunEmailProvider, EmailSender } from "wasp/email/core/types";
|
||||||
|
|
||||||
export function initMailgunEmailSender(
|
export function initMailgunEmailSender(
|
||||||
config: MailgunEmailProvider
|
config: MailgunEmailProvider
|
@ -1,6 +1,6 @@
|
|||||||
import SendGrid from "@sendgrid/mail";
|
import SendGrid from "@sendgrid/mail";
|
||||||
import { getDefaultFromField } from "../helpers.js";
|
import { getDefaultFromField } from "../helpers.js";
|
||||||
import type { SendGridProvider, EmailSender } from "../types.js";
|
import type { SendGridProvider, EmailSender } from "wasp/email/core/types";
|
||||||
|
|
||||||
export function initSendGridEmailSender(
|
export function initSendGridEmailSender(
|
||||||
provider: SendGridProvider
|
provider: SendGridProvider
|
@ -1,6 +1,6 @@
|
|||||||
import { createTransport } from "nodemailer";
|
import { createTransport } from "nodemailer";
|
||||||
import { formatFromField, getDefaultFromField } from "../helpers.js";
|
import { formatFromField, getDefaultFromField } from "../helpers.js";
|
||||||
import type { SMTPEmailProvider, EmailSender } from "../types.js";
|
import type { SMTPEmailProvider, EmailSender } from "wasp/email/core/types";
|
||||||
|
|
||||||
export function initSmtpEmailSender(config: SMTPEmailProvider): EmailSender {
|
export function initSmtpEmailSender(config: SMTPEmailProvider): EmailSender {
|
||||||
const transporter = createTransport({
|
const transporter = createTransport({
|
14
waspc/data/Generator/templates/sdk/jobs/_jobTypes.ts
Normal file
14
waspc/data/Generator/templates/sdk/jobs/_jobTypes.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{{={= =}=}}
|
||||||
|
import prisma from 'wasp/server/dbClient'
|
||||||
|
import type { JSONValue, JSONObject } from 'wasp/server/_types/serialization'
|
||||||
|
import { type JobFn } from '{= jobExecutorTypesImportPath =}'
|
||||||
|
|
||||||
|
{=! Used in framework code, shouldn't be public =}
|
||||||
|
export const entities = {
|
||||||
|
{=# entities =}
|
||||||
|
{= name =}: prisma.{= prismaIdentifier =},
|
||||||
|
{=/ entities =}
|
||||||
|
};
|
||||||
|
|
||||||
|
{=! Used by users, should be public =}
|
||||||
|
export type {= typeName =}<Input extends JSONObject, Output extends JSONValue | void> = JobFn<Input, Output, typeof entities>
|
8
waspc/data/Generator/templates/sdk/jobs/pgBoss/types.ts
Normal file
8
waspc/data/Generator/templates/sdk/jobs/pgBoss/types.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { PrismaDelegate } from 'wasp/server/_types'
|
||||||
|
import type { JSONValue, JSONObject } from 'wasp/server/_types/serialization'
|
||||||
|
|
||||||
|
export type JobFn<
|
||||||
|
Input extends JSONObject,
|
||||||
|
Output extends JSONValue | void,
|
||||||
|
Entities extends Partial<PrismaDelegate>
|
||||||
|
> = (data: Input, context: { entities: Entities }) => Promise<Output>
|
@ -3,7 +3,7 @@ import { HttpMethod } from 'wasp/types'
|
|||||||
import {
|
import {
|
||||||
serialize as superjsonSerialize,
|
serialize as superjsonSerialize,
|
||||||
deserialize as superjsonDeserialize,
|
deserialize as superjsonDeserialize,
|
||||||
} from 'superjson'
|
} from 'superjson'
|
||||||
|
|
||||||
export type OperationRoute = { method: HttpMethod, path: string }
|
export type OperationRoute = { method: HttpMethod, path: string }
|
||||||
|
|
@ -9,23 +9,142 @@
|
|||||||
"types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist"
|
"types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
"./core/HttpError": "./core/HttpError.js",
|
{=! todo(filip): Check all exports when done with SDK generation =}
|
||||||
"./core/AuthError": "./core/AuthError.js",
|
{=! Some of the statements in the comments might become incorrect. =}
|
||||||
"./core/config": "./core/config.js",
|
{=! "our code" means: "web-app", "server" or "SDK", or "some combination of the three". =}
|
||||||
"./core/stitches.config": "./core/stitches.config.js",
|
{=! Used by users, documented. =}
|
||||||
"./core/storage": "./core/storage.ts",
|
"./core/HttpError": "./dist/core/HttpError.js",
|
||||||
"./rpc": "./rpc/index.ts",
|
{=! Used by users, documented. =}
|
||||||
"./rpc/queries": "./rpc/queries/index.ts",
|
"./core/AuthError": "./dist/core/AuthError.js",
|
||||||
"./rpc/actions": "./rpc/actions/index.ts",
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
"./rpc/queryClient": "./rpc/queryClient.ts",
|
"./core/config": "./dist/core/config.js",
|
||||||
"./types": "./types/index.ts",
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
"./auth/*": "./auth/*",
|
"./core/stitches.config": "./dist/core/stitches.config.js",
|
||||||
"./api": "./api/index.ts",
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
"./api/*": "./api/*",
|
"./core/storage": "./dist/core/storage.js",
|
||||||
"./operations": "./operations/index.ts",
|
"./core/auth": "./dist/core/auth.js",
|
||||||
"./operations/*": "./operations/*",
|
{=! Used by users, documented. =}
|
||||||
"./universal/url": "./universal/url.ts",
|
"./rpc": "./dist/rpc/index.js",
|
||||||
"./universal/types": "./universal/url.ts"
|
{=! Used by users, documented. =}
|
||||||
|
"./rpc/queries": "./dist/rpc/queries/index.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./rpc/queries/core": "./dist/rpc/queries/core.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./rpc/actions": "./dist/rpc/actions/index.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./rpc/actions/core": "./dist/rpc/actions/core.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./rpc/queryClient": "./dist/rpc/queryClient.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./types": "./dist/types/index.js",
|
||||||
|
{=! Used by user, documented. =}
|
||||||
|
"./auth": "./dist/auth/index.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/types": "./dist/auth/types.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/login": "./dist/auth/login.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/logout": "./dist/auth/logout.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/signup": "./dist/auth/signup.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/useAuth": "./dist/auth/useAuth.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/user": "./dist/auth/user.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/email": "./dist/auth/email/index.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./auth/helpers/user": "./dist/auth/helpers/user.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./auth/session": "./dist/auth/session.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./auth/providers/types": "./dist/auth/providers/types.js",
|
||||||
|
{=! Used by user, documented. =}
|
||||||
|
"./auth/utils": "./dist/auth/utils.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./auth/password": "./dist/auth/password.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./auth/jwt": "./dist/auth/jwt.js",
|
||||||
|
{=! Used by user, documented. =}
|
||||||
|
"./auth/validation": "./dist/auth/validation.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/forms/Login": "./dist/auth/forms/Login.jsx",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/forms/Signup": "./dist/auth/forms/Signup.jsx",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/forms/VerifyEmail": "./dist/auth/forms/VerifyEmail.jsx",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/forms/ForgotPassword": "./dist/auth/forms/ForgotPassword.jsx",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/forms/ResetPassword": "./dist/auth/forms/ResetPassword.jsx",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/forms/internal/Form": "./dist/auth/forms/internal/Form.jsx",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./auth/helpers/*": "./dist/auth/helpers/*.jsx",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./auth/pages/createAuthRequiredPage": "./dist/auth/pages/createAuthRequiredPage.jsx",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./api": "./dist/api/index.js",
|
||||||
|
{=! Used by our framework code (Websockets), undocumented (but accessible) for users. =}
|
||||||
|
"./api/events": "./dist/api/events.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./operations": "./dist/operations/index.js",
|
||||||
|
{=! If we import a symbol like "import something form 'wasp/something'", we must =}
|
||||||
|
{=! expose it here (which leaks it to our users). We could avoid this by =}
|
||||||
|
{=! using relative imports inside SDK code (instead of library imports), =}
|
||||||
|
{=! but I didn't have time to implement it. =}
|
||||||
|
"./ext-src/*": "./dist/ext-src/*.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./operations/*": "./dist/operations/*",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./universal/url": "./dist/universal/url.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./universal/types": "./dist/universal/types.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./universal/validators": "./dist/universal/validators.js",
|
||||||
|
{=! Used by users and by our code, documented =}
|
||||||
|
"./server/dbClient": "./dist/server/dbClient.js",
|
||||||
|
{=! Used by users and by our code, documented. =}
|
||||||
|
"./server/config": "./dist/server/config.js",
|
||||||
|
{=! Used by users and by our code, documented. =}
|
||||||
|
"./server/types": "./dist/server/types/index.js",
|
||||||
|
{=! Used by users and by our code, documented. =}
|
||||||
|
"./server/middleware": "./dist/server/middleware/index.js",
|
||||||
|
{=! Parts are used by users, documented. Parts are probably used by our code, undocumented (but accessible). =}
|
||||||
|
"./server/utils": "./dist/server/utils.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./server/actions": "./dist/server/actions/index.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./server/queries": "./dist/server/queries/index.js",
|
||||||
|
{=! Used by users and also by our code, documented. =}
|
||||||
|
"./server/auth/email": "./dist/server/auth/email/index.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./dbSeed/types": "./dist/dbSeed/types.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./test": "./dist/test/index.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./test/*": "./dist/test/*.js",
|
||||||
|
"./crud/*": "./dist/crud/*.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./server/crud/*": "./dist/server/crud/*",
|
||||||
|
"./email": "./dist/email/index.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./email/core/types": "./dist/email/core/types.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./server/auth/email/utils": "./dist/server/auth/email/utils.js",
|
||||||
|
{=! Parts are used by users and documented (types), other parts are used by the framework code (entities). =}
|
||||||
|
"./jobs/*": "./dist/jobs/*.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
{=! Todo(filip): This export becomes problematic once we start supporting different executors =}
|
||||||
|
"./jobs/pgBoss/types": "./dist/jobs/pgBoss/types.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./router": "./dist/router/index.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./server/webSocket": "./dist/server/webSocket/index.js",
|
||||||
|
{=! Used by users, documented. =}
|
||||||
|
"./webSocket": "./dist/webSocket/index.js",
|
||||||
|
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||||
|
"./webSocket/WebSocketProvider": "./dist/webSocket/WebSocketProvider.jsx"
|
||||||
},
|
},
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"include": [
|
"include": [
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { Link as RouterLink } from 'react-router-dom'
|
import { Link as RouterLink } from 'react-router-dom'
|
||||||
import { type Routes } from '../router'
|
|
||||||
import { interpolatePath } from './linkHelpers'
|
import { interpolatePath } from './linkHelpers'
|
||||||
|
import { type Routes } from './index'
|
||||||
|
|
||||||
type RouterLinkProps = Parameters<typeof RouterLink>[0]
|
type RouterLinkProps = Parameters<typeof RouterLink>[0]
|
||||||
|
|
31
waspc/data/Generator/templates/sdk/router/index.ts
Normal file
31
waspc/data/Generator/templates/sdk/router/index.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{{={= =}=}}
|
||||||
|
import { interpolatePath } from './linkHelpers'
|
||||||
|
import type {
|
||||||
|
RouteDefinitionsToRoutes,
|
||||||
|
OptionalRouteOptions,
|
||||||
|
ParamValue,
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
export const routes = {
|
||||||
|
{=# routes =}
|
||||||
|
{= name =}: {
|
||||||
|
to: "{= urlPath =}",
|
||||||
|
{=# hasUrlParams =}
|
||||||
|
build: (
|
||||||
|
options: {
|
||||||
|
params: {{=# urlParams =}{= name =}{=# isOptional =}?{=/ isOptional =}: ParamValue;{=/ urlParams =}}
|
||||||
|
} & OptionalRouteOptions,
|
||||||
|
) => interpolatePath("{= urlPath =}", options.params, options?.search, options?.hash),
|
||||||
|
{=/ hasUrlParams =}
|
||||||
|
{=^ hasUrlParams =}
|
||||||
|
build: (
|
||||||
|
options?: OptionalRouteOptions,
|
||||||
|
) => interpolatePath("{= urlPath =}", undefined, options?.search, options?.hash),
|
||||||
|
{=/ hasUrlParams =}
|
||||||
|
},
|
||||||
|
{=/ routes =}
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type Routes = RouteDefinitionsToRoutes<typeof routes>
|
||||||
|
|
||||||
|
export { Link } from './Link'
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user