mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-12-26 10:35:04 +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 qualified Wasp.Message as Msg
|
||||
import Wasp.Project (CompileError, CompileWarning, WaspProjectDir)
|
||||
import Wasp.Project.Common
|
||||
( buildDirInDotWaspDir,
|
||||
dotWaspDirInWaspProjectDir,
|
||||
extClientCodeDirInWaspProjectDir,
|
||||
extServerCodeDirInWaspProjectDir,
|
||||
extSharedCodeDirInWaspProjectDir,
|
||||
)
|
||||
import Wasp.Project.Common (buildDirInDotWaspDir, dotWaspDirInWaspProjectDir)
|
||||
|
||||
-- | 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
|
||||
@ -71,9 +65,7 @@ buildIO waspProjectDir buildDir = compileIOWithOptions options waspProjectDir bu
|
||||
where
|
||||
options =
|
||||
CompileOptions
|
||||
{ externalClientCodeDirPath = waspProjectDir </> extClientCodeDirInWaspProjectDir,
|
||||
externalServerCodeDirPath = waspProjectDir </> extServerCodeDirInWaspProjectDir,
|
||||
externalSharedCodeDirPath = waspProjectDir </> extSharedCodeDirInWaspProjectDir,
|
||||
{ waspProjectDirPath = waspProjectDir,
|
||||
isBuild = True,
|
||||
sendMessage = cliSendMessage,
|
||||
-- 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 Wasp.Project (CompileError, CompileWarning, WaspProjectDir)
|
||||
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.
|
||||
compile :: Command [CompileWarning]
|
||||
@ -115,9 +115,7 @@ compileIOWithOptions options waspProjectDir outDir =
|
||||
defaultCompileOptions :: Path' Abs (Dir WaspProjectDir) -> CompileOptions
|
||||
defaultCompileOptions waspProjectDir =
|
||||
CompileOptions
|
||||
{ externalServerCodeDirPath = waspProjectDir </> extServerCodeDirInWaspProjectDir,
|
||||
externalClientCodeDirPath = waspProjectDir </> extClientCodeDirInWaspProjectDir,
|
||||
externalSharedCodeDirPath = waspProjectDir </> extSharedCodeDirInWaspProjectDir,
|
||||
{ waspProjectDirPath = waspProjectDir,
|
||||
isBuild = False,
|
||||
sendMessage = cliSendMessage,
|
||||
generatorWarningsFilter = id
|
||||
|
@ -15,9 +15,12 @@ import Wasp.Cli.Command.Message (cliSendMessageC)
|
||||
import Wasp.Cli.Command.Require (InWaspProject (InWaspProject), require)
|
||||
import Wasp.Cli.Command.Watch (watch)
|
||||
import qualified Wasp.Generator
|
||||
import Wasp.Generator.Common (ProjectRootDir)
|
||||
import qualified Wasp.Message as Msg
|
||||
import Wasp.Project.Common (dotWaspDirInWaspProjectDir, generatedCodeDirInDotWaspDir)
|
||||
import Wasp.Project.Common
|
||||
( WaspProjectDir,
|
||||
dotWaspDirInWaspProjectDir,
|
||||
generatedCodeDirInDotWaspDir,
|
||||
)
|
||||
|
||||
test :: [String] -> Command ()
|
||||
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 _ = 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
|
||||
InWaspProject waspRoot <- require
|
||||
let outDir = waspRoot </> dotWaspDirInWaspProjectDir </> generatedCodeDirInDotWaspDir
|
||||
@ -39,7 +42,7 @@ watchAndTest testRunner = do
|
||||
watchOrStartResult <- liftIO $ do
|
||||
ongoingCompilationResultMVar <- newMVar (warnings, [])
|
||||
let watchWaspProjectSource = watch waspRoot outDir ongoingCompilationResultMVar
|
||||
watchWaspProjectSource `race` testRunner outDir
|
||||
watchWaspProjectSource `race` testRunner waspRoot
|
||||
|
||||
case watchOrStartResult of
|
||||
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.Message as Msg
|
||||
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
|
||||
-- .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
|
||||
chan <- newChan
|
||||
_ <- watchFilesAtTopLevelOfWaspProjectDir mgr chan
|
||||
_ <- watchFilesAtAllLevelsOfSrcDirInWaspProjectDir mgr chan
|
||||
_ <- watchFilesAtAllLevelsOfDirInWaspProjectDir mgr chan srcDirInWaspProjectDir
|
||||
_ <- watchFilesAtAllLevelsOfDirInWaspProjectDir mgr chan extPublicDirInWaspProjectDir
|
||||
listenForEvents chan =<< getCurrentTime
|
||||
where
|
||||
watchFilesAtTopLevelOfWaspProjectDir mgr chan =
|
||||
@ -53,8 +54,8 @@ watch waspProjectDir outDir ongoingCompilationResultMVar = FSN.withManager $ \mg
|
||||
where
|
||||
filename = FP.takeFileName $ FSN.eventPath event
|
||||
|
||||
watchFilesAtAllLevelsOfSrcDirInWaspProjectDir mgr chan =
|
||||
FSN.watchTreeChan mgr (SP.fromAbsDir $ waspProjectDir </> srcDirInWaspProjectDir) eventFilter chan
|
||||
watchFilesAtAllLevelsOfDirInWaspProjectDir mgr chan dirInWaspProjectDir =
|
||||
FSN.watchTreeChan mgr (SP.fromAbsDir $ waspProjectDir </> dirInWaspProjectDir) eventFilter chan
|
||||
where
|
||||
eventFilter :: FSN.Event -> Bool
|
||||
eventFilter event =
|
||||
|
@ -5,6 +5,8 @@
|
||||
"react": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.1.0",
|
||||
"vite": "^4.3.9",
|
||||
"@types/react": "^18.0.37",
|
||||
"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...");
|
||||
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 { useHistory } from 'react-router-dom'
|
||||
|
||||
import config from '../../config.js'
|
||||
import api from '../../api'
|
||||
import { initSession } from '../helpers/user'
|
||||
import config from 'wasp/core/config'
|
||||
import api from 'wasp/api'
|
||||
import { initSession } from 'wasp/auth/helpers/user'
|
||||
|
||||
// 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
|
||||
|
@ -2,7 +2,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import { Redirect } from 'react-router-dom'
|
||||
import useAuth from '../useAuth'
|
||||
import useAuth from 'wasp/auth/useAuth'
|
||||
|
||||
|
||||
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 =}
|
||||
|
||||
{=# areWebSocketsUsed =}
|
||||
import { WebSocketProvider } from './webSocket/WebSocketProvider'
|
||||
import { WebSocketProvider } from 'wasp/webSocket/WebSocketProvider'
|
||||
{=/ areWebSocketsUsed =}
|
||||
|
||||
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 { 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.importStatement =}
|
||||
{=/ rootComponent.isDefined =}
|
||||
|
||||
{=# isAuthEnabled =}
|
||||
import createAuthRequiredPage from "wasp/auth/pages/createAuthRequiredPage"
|
||||
import createAuthRequiredPage from "./auth/pages/createAuthRequiredPage"
|
||||
{=/ isAuthEnabled =}
|
||||
|
||||
{=# pagesToImport =}
|
||||
@ -23,29 +17,14 @@ import createAuthRequiredPage from "wasp/auth/pages/createAuthRequiredPage"
|
||||
import OAuthCodeExchange from "./auth/pages/OAuthCodeExchange"
|
||||
{=/ isExternalAuthEnabled =}
|
||||
|
||||
export const routes = {
|
||||
import { routes } from 'wasp/router'
|
||||
|
||||
export const routeNameToRouteComponent = {
|
||||
{=# routes =}
|
||||
{= name =}: {
|
||||
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 =}
|
||||
},
|
||||
{= name =}: {= targetComponent =},
|
||||
{=/ routes =}
|
||||
} as const;
|
||||
|
||||
export type Routes = RouteDefinitionsToRoutes<typeof routes>
|
||||
|
||||
const router = (
|
||||
<Router basename="{= baseDir =}">
|
||||
{=# rootComponent.isDefined =}
|
||||
@ -57,7 +36,7 @@ const router = (
|
||||
exact
|
||||
key={routeKey}
|
||||
path={route.to}
|
||||
component={route.component}
|
||||
component={routeNameToRouteComponent[routeKey]}
|
||||
/>
|
||||
))}
|
||||
{=# isExternalAuthEnabled =}
|
||||
@ -77,5 +56,3 @@ const router = (
|
||||
)
|
||||
|
||||
export default router
|
||||
|
||||
export { Link } from './router/Link'
|
||||
|
@ -1,4 +1,8 @@
|
||||
import matchers from '@testing-library/jest-dom/matchers'
|
||||
import { expect } from 'vitest'
|
||||
import { afterEach } 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,
|
||||
"strict": false,
|
||||
// Allow importing pages with the .tsx extension.
|
||||
"allowImportingTsExtensions": true
|
||||
"allowImportingTsExtensions": true,
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
@ -2,8 +2,11 @@
|
||||
/// <reference types="vitest" />
|
||||
import { mergeConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defaultExclude } from "vitest/config"
|
||||
|
||||
{=# customViteConfig.isDefined =}
|
||||
// Ignoring the TS error because we are importing a file outside of TS root dir.
|
||||
// @ts-ignore
|
||||
{=& customViteConfig.importStatement =}
|
||||
const _waspUserProvidedConfig = {=& customViteConfig.importIdentifier =}
|
||||
{=/ customViteConfig.isDefined =}
|
||||
@ -27,12 +30,16 @@ const defaultViteConfig = {
|
||||
outDir: "build",
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
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/
|
||||
|
@ -2,7 +2,7 @@ import axios, { type AxiosError } from 'axios'
|
||||
|
||||
import config from 'wasp/core/config'
|
||||
import { storage } from 'wasp/core/storage'
|
||||
import { apiEventsEmitter } from 'wasp/api/events'
|
||||
import { apiEventsEmitter } from './events.js'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: config.apiUrl,
|
@ -1,5 +1,5 @@
|
||||
{{={= =}=}}
|
||||
import api, { handleApiError } from '../../../api';
|
||||
import api, { handleApiError } from 'wasp/api';
|
||||
import { initSession } from '../../helpers/user';
|
||||
|
||||
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 }> {
|
||||
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 }> {
|
||||
try {
|
@ -1,5 +1,5 @@
|
||||
{{={= =}=}}
|
||||
import api, { handleApiError } from '../../../api'
|
||||
import api, { handleApiError } from 'wasp/api'
|
||||
|
||||
export async function verifyEmail(data: {
|
||||
token: string
|
@ -1,7 +1,7 @@
|
||||
{{={= =}=}}
|
||||
import { useState, createContext } from 'react'
|
||||
import { createTheme } from '@stitches/react'
|
||||
import { styled } from '../../stitches.config'
|
||||
import { styled } from 'wasp/core/stitches.config'
|
||||
|
||||
import {
|
||||
type State,
|
@ -1,8 +1,8 @@
|
||||
{{={= =}=}}
|
||||
import { useContext } from 'react'
|
||||
import { useForm, UseFormReturn } from 'react-hook-form'
|
||||
import { styled } from '../../../../stitches.config'
|
||||
import config from '../../../../config'
|
||||
import { styled } from 'wasp/core/stitches.config'
|
||||
import config from 'wasp/core/config'
|
||||
|
||||
import { AuthContext } from '../../Auth'
|
||||
import {
|
@ -1,4 +1,4 @@
|
||||
import { styled } from '../../../../stitches.config'
|
||||
import { styled } from 'wasp/core/stitches.config'
|
||||
|
||||
export const SocialButton = styled('a', {
|
||||
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 * as SocialIcons from '../forms/internal/social/SocialIcons'
|
||||
|
@ -1,7 +1,7 @@
|
||||
import jwt from 'jsonwebtoken'
|
||||
import util from 'util'
|
||||
|
||||
import config from '../config.js'
|
||||
import config from 'wasp/server/config'
|
||||
|
||||
const jwtSign = util.promisify(jwt.sign)
|
||||
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'
|
||||
|
||||
export default async function login(username: string, password: string): Promise<void> {
|
@ -1,9 +1,8 @@
|
||||
{{={= =}=}}
|
||||
import { Lucia } from "lucia";
|
||||
import { PrismaAdapter } from "@lucia-auth/adapter-prisma";
|
||||
import prisma from '../dbClient.js'
|
||||
import config from '../config.js'
|
||||
import { type {= userEntityUpper =} } from "../entities/index.js"
|
||||
import prisma from 'wasp/server/dbClient'
|
||||
import { type {= userEntityUpper =} } from "wasp/entities"
|
||||
|
||||
const prismaAdapter = new PrismaAdapter(
|
||||
// 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 { Prisma } from '@prisma/client'
|
||||
import type { Expand } from '../../universal/types'
|
||||
import type { Expand } from 'wasp/universal/types'
|
||||
import type { ProviderName } from '../utils'
|
||||
|
||||
type UserEntityCreateInput = Prisma.{= userEntityUpper =}CreateInput
|
@ -1,8 +1,8 @@
|
||||
{{={= =}=}}
|
||||
import { Request as ExpressRequest } from "express";
|
||||
|
||||
import { type {= userEntityUpper =} } from "../entities/index.js"
|
||||
import { type SanitizedUser } from '../_types/index.js'
|
||||
import { type {= userEntityUpper =} } from "wasp/entities"
|
||||
import { type SanitizedUser } from 'wasp/server/_types'
|
||||
|
||||
import { auth } from "./lucia.js";
|
||||
import type { Session } from "lucia";
|
||||
@ -11,7 +11,7 @@ import {
|
||||
deserializeAndSanitizeProviderData,
|
||||
} from "./utils.js";
|
||||
|
||||
import prisma from '../dbClient.js';
|
||||
import prisma from 'wasp/server/dbClient';
|
||||
|
||||
// Creates a new session for the `authId` in the database
|
||||
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> {
|
||||
try {
|
@ -1,8 +1,9 @@
|
||||
{{={= =}=}}
|
||||
import { deserialize as superjsonDeserialize } from 'superjson'
|
||||
import { useQuery } from 'wasp/rpc'
|
||||
import api, { handleApiError } from 'wasp/api'
|
||||
import { HttpMethod } from 'wasp/types'
|
||||
import type { User } from './types'
|
||||
import type { User } from './types'
|
||||
import { addMetadataToQuery } from 'wasp/rpc/queries'
|
||||
|
||||
export const getMe = createUserGetter()
|
||||
@ -31,7 +32,7 @@ function createUserGetter() {
|
||||
addMetadataToQuery(getMe, {
|
||||
relativeQueryPath: getMeRelativePath,
|
||||
queryRoute: getMeRoute,
|
||||
entitiesUsed: ['User'],
|
||||
entitiesUsed: {=& entitiesGetMeDependsOn =},
|
||||
})
|
||||
|
||||
return getMe
|
@ -1,8 +1,4 @@
|
||||
// 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'
|
||||
import type { User, ProviderName, DeserializedAuthIdentity } from './types'
|
||||
|
||||
export function getEmail(user: User): string | null {
|
||||
return findUserIdentity(user, "email")?.providerUserId ?? null;
|
@ -1,14 +1,15 @@
|
||||
{{={= =}=}}
|
||||
import { hashPassword } from './password.js'
|
||||
import { verify } from './jwt.js'
|
||||
import AuthError from '../core/AuthError.js'
|
||||
import HttpError from '../core/HttpError.js'
|
||||
import prisma from '../server/dbClient.js'
|
||||
import { sleep } from '../server/utils'
|
||||
import AuthError from 'wasp/core/AuthError'
|
||||
import HttpError from 'wasp/core/HttpError'
|
||||
import prisma from 'wasp/server/dbClient'
|
||||
import { sleep } from 'wasp/server/utils'
|
||||
import {
|
||||
type User,
|
||||
type Auth,
|
||||
type AuthIdentity,
|
||||
} from '../entities'
|
||||
type {= userEntityUpper =},
|
||||
type {= authEntityUpper =},
|
||||
type {= authIdentityEntityUpper =},
|
||||
} from 'wasp/entities'
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import { throwValidationError } from './validation.js'
|
||||
@ -46,13 +47,13 @@ export type ProviderName = keyof PossibleProviderData
|
||||
|
||||
export const contextWithUserEntity = {
|
||||
entities: {
|
||||
User: prisma.user
|
||||
{= userEntityUpper =}: prisma.{= userEntityLower =}
|
||||
}
|
||||
}
|
||||
|
||||
export const authConfig = {
|
||||
failureRedirectPath: "/login",
|
||||
successRedirectPath: "/",
|
||||
failureRedirectPath: "{= failureRedirectPath =}",
|
||||
successRedirectPath: "{= successRedirectPath =}",
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,8 +77,8 @@ export function createProviderId(providerName: ProviderName, providerUserId: str
|
||||
}
|
||||
}
|
||||
|
||||
export async function findAuthIdentity(providerId: ProviderId): Promise<AuthIdentity | null> {
|
||||
return prisma.authIdentity.findUnique({
|
||||
export async function findAuthIdentity(providerId: ProviderId): Promise<{= authIdentityEntityUpper =} | null> {
|
||||
return prisma.{= authIdentityEntityLower =}.findUnique({
|
||||
where: {
|
||||
providerName_providerUserId: providerId,
|
||||
}
|
||||
@ -96,7 +97,7 @@ export async function updateAuthIdentityProviderData<PN extends ProviderName>(
|
||||
providerId: ProviderId,
|
||||
existingProviderData: PossibleProviderData[PN],
|
||||
providerDataUpdates: Partial<PossibleProviderData[PN]>,
|
||||
): Promise<AuthIdentity> {
|
||||
): Promise<{= authIdentityEntityUpper =}> {
|
||||
// We are doing the sanitization here only on updates to avoid
|
||||
// hashing the password multiple times.
|
||||
const sanitizedProviderDataUpdates = await sanitizeProviderData(providerDataUpdates);
|
||||
@ -105,7 +106,7 @@ export async function updateAuthIdentityProviderData<PN extends ProviderName>(
|
||||
...sanitizedProviderDataUpdates,
|
||||
}
|
||||
const serializedProviderData = await serializeProviderData<PN>(newProviderData);
|
||||
return prisma.authIdentity.update({
|
||||
return prisma.{= authIdentityEntityLower =}.update({
|
||||
where: {
|
||||
providerName_providerUserId: providerId,
|
||||
},
|
||||
@ -113,31 +114,31 @@ export async function updateAuthIdentityProviderData<PN extends ProviderName>(
|
||||
});
|
||||
}
|
||||
|
||||
type FindAuthWithUserResult = Auth & {
|
||||
user: User
|
||||
type FindAuthWithUserResult = {= authEntityUpper =} & {
|
||||
{= userFieldOnAuthEntityName =}: {= userEntityUpper =}
|
||||
}
|
||||
|
||||
export async function findAuthWithUserBy(
|
||||
where: Prisma.AuthWhereInput
|
||||
where: Prisma.{= authEntityUpper =}WhereInput
|
||||
): Promise<FindAuthWithUserResult> {
|
||||
return prisma.auth.findFirst({ where, include: { user: true }});
|
||||
return prisma.{= authEntityLower =}.findFirst({ where, include: { {= userFieldOnAuthEntityName =}: true }});
|
||||
}
|
||||
|
||||
export async function createUser(
|
||||
providerId: ProviderId,
|
||||
serializedProviderData?: string,
|
||||
userFields?: PossibleUserFields,
|
||||
): Promise<User & {
|
||||
auth: Auth
|
||||
): Promise<{= userEntityUpper =} & {
|
||||
auth: {= authEntityUpper =}
|
||||
}> {
|
||||
return prisma.user.create({
|
||||
return prisma.{= userEntityLower =}.create({
|
||||
data: {
|
||||
// Using any here to prevent type errors when userFields are not
|
||||
// defined. We want Prisma to throw an error in that case.
|
||||
...(userFields ?? {} as any),
|
||||
auth: {
|
||||
{= authFieldOnUserEntityName =}: {
|
||||
create: {
|
||||
identities: {
|
||||
{= identitiesFieldOnAuthEntityName =}: {
|
||||
create: {
|
||||
providerName: providerId.providerName,
|
||||
providerUserId: providerId.providerUserId,
|
||||
@ -150,13 +151,13 @@ export async function createUser(
|
||||
// We need to include the Auth entity here because we need `authId`
|
||||
// to be able to create a session.
|
||||
include: {
|
||||
auth: true,
|
||||
{= authFieldOnUserEntityName =}: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteUserByAuthId(authId: string): Promise<{ count: number }> {
|
||||
return prisma.user.deleteMany({ where: { auth: {
|
||||
return prisma.{= userEntityLower =}.deleteMany({ where: { auth: {
|
||||
id: authId,
|
||||
} } })
|
||||
}
|
||||
@ -213,7 +214,7 @@ export function rethrowPossibleAuthError(e: unknown): void {
|
||||
// Prisma code P2003 is for foreign key constraint failure
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2003') {
|
||||
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".
|
||||
Read more at: https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/referential-actions`)
|
||||
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';
|
||||
const USERNAME_FIELD = 'username';
|
@ -1,5 +1,7 @@
|
||||
class AuthError extends Error {
|
||||
constructor (message, data, ...params) {
|
||||
public data: unknown
|
||||
|
||||
constructor (message: string, data?: unknown, ...params: unknown[]) {
|
||||
super(message, ...params)
|
||||
|
||||
if (Error.captureStackTrace) {
|
@ -1,5 +1,8 @@
|
||||
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)
|
||||
|
||||
if (Error.captureStackTrace) {
|
@ -1,7 +1,4 @@
|
||||
import { randomInt } from 'node:crypto'
|
||||
|
||||
import prisma from '../server/dbClient.js'
|
||||
import { handleRejection } from '../utils.js'
|
||||
import { handleRejection } from 'wasp/server/utils'
|
||||
import { getSessionAndUserFromBearerToken } from 'wasp/auth/session'
|
||||
import { throwInvalidCredentialsError } from 'wasp/auth/utils'
|
||||
|
@ -1,8 +1,8 @@
|
||||
{{={= =}=}}
|
||||
import { createAction } from "../actions/core";
|
||||
import { useAction } from "../actions";
|
||||
import { createQuery } from "../queries/core";
|
||||
import { useQuery } from "../queries";
|
||||
import { createAction } from "wasp/rpc/actions/core";
|
||||
import { useAction } from "wasp/rpc";
|
||||
import { createQuery } from "wasp/rpc/queries/core";
|
||||
import { useQuery } from "wasp/rpc";
|
||||
import {
|
||||
{=# operations.Get =}
|
||||
GetQueryResolved,
|
||||
@ -19,7 +19,7 @@ import {
|
||||
{=# operations.Delete =}
|
||||
DeleteActionResolved,
|
||||
{=/ operations.Delete =}
|
||||
} from '../../../server/src/crud/{= name =}'
|
||||
} from 'wasp/server/crud/{= name =}'
|
||||
|
||||
function createCrud() {
|
||||
{=# 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
|
||||
// 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";
|
||||
|
||||
const yellowColor = "\x1b[33m%s\x1b[0m";
|
@ -1,6 +1,6 @@
|
||||
import { NodeMailgun } from "ts-mailgun";
|
||||
import { getDefaultFromField } from "../helpers.js";
|
||||
import type { MailgunEmailProvider, EmailSender } from "../types.js";
|
||||
import type { MailgunEmailProvider, EmailSender } from "wasp/email/core/types";
|
||||
|
||||
export function initMailgunEmailSender(
|
||||
config: MailgunEmailProvider
|
@ -1,6 +1,6 @@
|
||||
import SendGrid from "@sendgrid/mail";
|
||||
import { getDefaultFromField } from "../helpers.js";
|
||||
import type { SendGridProvider, EmailSender } from "../types.js";
|
||||
import type { SendGridProvider, EmailSender } from "wasp/email/core/types";
|
||||
|
||||
export function initSendGridEmailSender(
|
||||
provider: SendGridProvider
|
@ -1,6 +1,6 @@
|
||||
import { createTransport } from "nodemailer";
|
||||
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 {
|
||||
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 {
|
||||
serialize as superjsonSerialize,
|
||||
deserialize as superjsonDeserialize,
|
||||
} from 'superjson'
|
||||
} from 'superjson'
|
||||
|
||||
export type OperationRoute = { method: HttpMethod, path: string }
|
||||
|
@ -9,23 +9,142 @@
|
||||
"types": "tsc --declaration --emitDeclarationOnly --stripInternal --declarationDir dist"
|
||||
},
|
||||
"exports": {
|
||||
"./core/HttpError": "./core/HttpError.js",
|
||||
"./core/AuthError": "./core/AuthError.js",
|
||||
"./core/config": "./core/config.js",
|
||||
"./core/stitches.config": "./core/stitches.config.js",
|
||||
"./core/storage": "./core/storage.ts",
|
||||
"./rpc": "./rpc/index.ts",
|
||||
"./rpc/queries": "./rpc/queries/index.ts",
|
||||
"./rpc/actions": "./rpc/actions/index.ts",
|
||||
"./rpc/queryClient": "./rpc/queryClient.ts",
|
||||
"./types": "./types/index.ts",
|
||||
"./auth/*": "./auth/*",
|
||||
"./api": "./api/index.ts",
|
||||
"./api/*": "./api/*",
|
||||
"./operations": "./operations/index.ts",
|
||||
"./operations/*": "./operations/*",
|
||||
"./universal/url": "./universal/url.ts",
|
||||
"./universal/types": "./universal/url.ts"
|
||||
{=! todo(filip): Check all exports when done with SDK generation =}
|
||||
{=! Some of the statements in the comments might become incorrect. =}
|
||||
{=! "our code" means: "web-app", "server" or "SDK", or "some combination of the three". =}
|
||||
{=! Used by users, documented. =}
|
||||
"./core/HttpError": "./dist/core/HttpError.js",
|
||||
{=! Used by users, documented. =}
|
||||
"./core/AuthError": "./dist/core/AuthError.js",
|
||||
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||
"./core/config": "./dist/core/config.js",
|
||||
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||
"./core/stitches.config": "./dist/core/stitches.config.js",
|
||||
{=! Used by our code, uncodumented (but accessible) for users. =}
|
||||
"./core/storage": "./dist/core/storage.js",
|
||||
"./core/auth": "./dist/core/auth.js",
|
||||
{=! Used by users, documented. =}
|
||||
"./rpc": "./dist/rpc/index.js",
|
||||
{=! 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",
|
||||
"include": [
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Link as RouterLink } from 'react-router-dom'
|
||||
import { type Routes } from '../router'
|
||||
import { interpolatePath } from './linkHelpers'
|
||||
import { type Routes } from './index'
|
||||
|
||||
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