Compare commits

..

33 Commits

Author SHA1 Message Date
Szilárd Dóró
3c1996b13b Merge pull request #1543 from nhost/changeset-release/main
chore: update versions
2023-01-30 10:01:21 +01:00
github-actions[bot]
e3d90fd5d2 chore: update versions 2023-01-30 08:58:53 +00:00
Szilárd Dóró
af016e1caa Merge pull request #1548 from nhost/remove-functions-section
fix(dashboard): removed functions section
2023-01-30 09:57:31 +01:00
Szilárd Dóró
ed4c780115 fix(dashboard): lint error 2023-01-30 09:40:51 +01:00
Johan Eliasson
941f0f5755 removed unused code 2023-01-28 20:17:43 +01:00
Johan Eliasson
75344b2bc0 remove 2023-01-28 18:02:57 +01:00
Johan Eliasson
65da426e8b update 2023-01-28 17:46:18 +01:00
Johan Eliasson
f34702f3c5 Merge pull request #1541 from nhost/docs-graphql-integrations
stripe graphql api updates
2023-01-27 19:35:31 +01:00
Johan Eliasson
6cb70eee01 Update docs/docs/graphql/remote-schemas/stripe.mdx
Co-authored-by: Guido Curcio <guidomaurocurcio@gmail.com>
2023-01-27 16:12:43 +01:00
Johan Eliasson
9395c9687f update 2023-01-27 16:03:31 +01:00
Johan Eliasson
eb1eb934a4 update 2023-01-27 10:36:56 +01:00
Johan Eliasson
c62fed2c9a update 2023-01-27 09:16:24 +01:00
Johan Eliasson
16fe1a47da fixed broken links 2023-01-27 09:13:42 +01:00
Johan Eliasson
0f04e8b8b8 added example response 2023-01-27 09:07:38 +01:00
Johan Eliasson
e6dad4d696 added changeset 2023-01-27 08:57:30 +01:00
Johan Eliasson
bcb3b79add updates 2023-01-27 08:54:23 +01:00
Szilárd Dóró
fe658231b4 Merge pull request #1538 from nhost/changeset-release/main
chore: update versions
2023-01-23 10:37:11 +01:00
github-actions[bot]
a1188b7d98 chore: update versions 2023-01-23 09:10:50 +00:00
Szilárd Dóró
cd4bdc581d Merge pull request #1537 from nhost/fix/local-auth-page
fix(dashboard): don't break Auth page in local mode
2023-01-23 10:09:26 +01:00
Szilárd Dóró
4e2f8ccd52 fix(dashboard): don't break Auth page in local mode 2023-01-23 09:30:11 +01:00
Szilárd Dóró
8a6d8c7534 Merge pull request #1534 from nhost/changeset-release/main
chore: update versions
2023-01-19 08:17:58 +01:00
github-actions[bot]
fa75409f09 chore: update versions 2023-01-18 20:37:34 +00:00
Szilárd Dóró
74662052ae Merge pull request #1531 from nhost/fix/allowed-emails-and-domains
fix(dashboard): enable toggle when settings are filled in
2023-01-18 21:36:20 +01:00
Szilárd Dóró
37ab5fe878 trigger build 2023-01-18 17:50:28 +01:00
Szilárd Dóró
be9af96fa7 fix(dashboard): remove values if toggle is disabled 2023-01-18 16:42:44 +01:00
Szilárd Dóró
31abbe5f30 fix(dashboard): enable toggle when settings are filled in 2023-01-18 15:18:39 +01:00
Szilárd Dóró
268b461d5b Merge pull request #1529 from nhost/changeset-release/main
chore: update versions
2023-01-18 10:44:26 +01:00
github-actions[bot]
58af592cfa chore: update versions 2023-01-18 09:04:47 +00:00
Szilárd Dóró
0e9d623c69 Merge pull request #1527 from nhost/fix/permission-editor-array-input
fix(dashboard): don't throw validation error for valid permission rules
2023-01-18 10:03:35 +01:00
Szilárd Dóró
412a290646 Merge pull request #1528 from nhost/chore/lower-storage-page-limit
chore(dashboard): list fewer images per page on the Storage page
2023-01-18 09:35:15 +01:00
Szilárd Dóró
123add38a4 fix(dashboard): fetch images from correct URL 2023-01-17 16:43:34 +01:00
Szilárd Dóró
5bdd31ad36 chore(dashboard): list fewer images per page on the Storage page 2023-01-17 16:41:14 +01:00
Szilárd Dóró
5121851c8b fix(dashboard): don't throw validation error for valid permission rules 2023-01-17 16:35:29 +01:00
41 changed files with 647 additions and 1196 deletions

View File

@@ -1,5 +1,30 @@
# @nhost/dashboard
## 0.10.0
### Minor Changes
- ed4c7801: chore(dashboard): remove Functions section
## 0.9.10
### Patch Changes
- 4e2f8ccd: fix(dashboard): don't break Auth page in local mode
## 0.9.9
### Patch Changes
- 31abbe5f: fix(dashboard): enable toggle when settings are filled in
## 0.9.8
### Patch Changes
- 5bdd31ad: chore(dashboard): list fewer images per page on the Storage page
- 5121851c: fix(dashboard): don't throw validation error for valid permission rules
## 0.9.7
### Patch Changes

View File

@@ -1,6 +1,6 @@
# Nhost Dashboard
This is the Nhost Dashboard, a web application that allows you to manage your Nhost project.
This is the Nhost Dashboard, a web application that allows you to manage your Nhost projects.
To get started, you need to have an Nhost project. If you don't have one, you can [create a project here](https://app.nhost.io).
```bash

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/dashboard",
"version": "0.9.7",
"version": "0.10.0",
"private": true,
"scripts": {
"preinstall": "npx only-allow pnpm",
@@ -8,7 +8,7 @@
"build": "next build --no-lint",
"analyze": "ANALYZE=true pnpm build --no-lint",
"start": "next start",
"lint": "next lint --max-warnings 3",
"lint": "next lint --max-warnings 2",
"test": "vitest",
"codegen": "graphql-codegen --config graphql.config.yaml --errors-only",
"nhost:dev": "nhost dev -d",

View File

@@ -1,5 +0,0 @@
export type FunctionLog = {
name: string;
language: string;
logs: { date: string; message: string; createdAt: string }[];
};

View File

@@ -1,30 +0,0 @@
import { Text } from '@/ui/Text';
import { ChevronRightIcon } from '@heroicons/react/solid';
import { formatDistance } from 'date-fns';
export interface FunctionLogDataEntryProps {
time: string;
nav: string;
}
export function FunctionLogDataEntry({ time, nav }: FunctionLogDataEntryProps) {
return (
<a href={`#${nav}`}>
<div className="flex cursor-pointer flex-row place-content-between border-t py-3">
<Text
color="greyscaleDark"
variant="body"
className="flex font-medium"
size="tiny"
>
{formatDistance(new Date(time), new Date(), {
addSuffix: true,
})}
</Text>
<ChevronRightIcon className="ml-2 h-4 w-4 cursor-pointer self-center text-greyscaleDark" />
</div>
</a>
);
}
export default FunctionLogDataEntry;

View File

@@ -1,52 +0,0 @@
import { Text } from '@/ui/Text';
import { FunctionLogDataEntry } from './FunctionLogDataEntry';
export interface FunctionLogHistoryProps {
logs?: Log[];
}
type Log = {
createdAt: string;
date: any;
message: string;
};
export function FunctionLogHistory({ logs }: FunctionLogHistoryProps) {
return (
<div className=" mx-auto max-w-6xl pt-10">
<div className="flex flex-row place-content-between">
<div className="flex">
<Text size="large" className="font-medium" color="greyscaleDark">
Log History
</Text>
</div>
</div>
<div className="mt-5 flex flex-col">
<div className="flex flex-row">
<Text className="font-semibold" size="normal" color="greyscaleDark">
Time
</Text>
</div>
<div className="flex flex-col">
{logs ? (
<div>
{logs.slice(0, 4).map((log: Log) => (
<FunctionLogDataEntry
time={log.createdAt}
nav={`#-${log.date}`}
key={`${log.date}-${log.message.slice(66)}`}
/>
))}
</div>
) : (
<div className="pt-1 pl-0.5 font-mono text-xs text-greyscaleDark">
No log history.
</div>
)}
</div>
</div>
</div>
);
}
export default FunctionLogHistory;

View File

@@ -1,89 +0,0 @@
import { normalizeToIndividualFunctionsWithLogs } from '@/components/applications/functions/normalizeToIndividualFunctionsWithLogs';
import terminalTheme from '@/data/terminalTheme';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useGetFunctionLogQuery } from '@/utils/__generated__/graphql';
import { useEffect, useState } from 'react';
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
import json from 'react-syntax-highlighter/dist/cjs/languages/hljs/json';
import { FunctionLogHistory } from './FunctionLogHistory';
SyntaxHighlighter.registerLanguage('json', json);
export function FunctionsLogsTerminalPage({ functionName }: any) {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [normalizedFunctionData, setNormalizedFunctionData] = useState(null);
const { data, startPolling } = useGetFunctionLogQuery({
variables: {
subdomain: currentApplication.subdomain,
functionPaths: [functionName?.split('/').slice(1, 3).join('/')],
},
});
useEffect(() => {
startPolling(3000);
}, [startPolling]);
useEffect(() => {
if (!data || data.getFunctionLogs.length === 0) {
return;
}
setNormalizedFunctionData(
normalizeToIndividualFunctionsWithLogs(data.getFunctionLogs)[0],
);
}, [data]);
if (
!data ||
data.getFunctionLogs.length === 0 ||
!normalizedFunctionData ||
normalizedFunctionData.logs.length === 0
) {
return (
<div className="w-full rounded-lg text-white">
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
<div className="font-mono text-xs text-grey">
There are no stored logs yet. Try calling your function for logs to
appear.
</div>
</div>
<FunctionLogHistory />
</div>
);
}
return (
<div className="w-full rounded-lg text-white">
<div className="h-terminal overflow-auto rounded-lg bg-log px-4 py-4 font-mono shadow-sm">
{normalizedFunctionData.logs.map((log) => (
<div
key={`${log.date}-${log.message.slice(66)}`}
className=" flex text-sm"
>
<div id={`#-${log.date}`}>
<pre className="inline">
<span className="mr-4 text-greyscaleGrey">{log.date}</span>{' '}
<span className="">
{' '}
<SyntaxHighlighter
style={terminalTheme}
customStyle={{
display: 'inline',
}}
className="inline-flex"
language="json"
>
{log.message}
</SyntaxHighlighter>
</span>
</pre>
</div>
</div>
))}
</div>
<FunctionLogHistory logs={normalizedFunctionData.logs} />
</div>
);
}
export default FunctionsLogsTerminalPage;

View File

@@ -1,5 +0,0 @@
export type FunctionResponseLog = {
functionPath: string;
createdAt: string;
message: string;
};

View File

@@ -1,46 +0,0 @@
import { Button } from '@/ui/Button';
import Loading from '@/ui/Loading';
import { Text } from '@/ui/Text';
import Image from 'next/image';
export function FunctionsNotDeployed() {
return (
<div className="mx-auto mt-12 max-w-2xl text-center">
<div className="mx-auto flex w-centImage flex-col text-center">
<Image
src="/terminal-text.svg"
alt="Terminal with a green dot"
width={72}
height={72}
/>
</div>
<Text className="mt-4 font-medium" size="large" color="dark">
Functions Logs
</Text>
<Text size="normal" color="greyscaleDark" className="mt-1 transform">
Once you deploy a function, you can view the logs here.
</Text>
<div className="mt-1.5 flex text-center">
<Button
Component="a"
transparent
color="blue"
className="mx-auto cursor-pointer font-medium"
href="https://docs.nhost.io/platform/serverless-functions"
target="_blank"
rel="noreferrer"
>
Read more
</Button>
</div>
<div className="mt-24 flex flex-col text-center">
<Loading />
<Text size="normal" color="greyscaleDark" className="mt-1 transform">
Awaiting new requests
</Text>
</div>
</div>
);
}
export default FunctionsNotDeployed;

View File

@@ -1,79 +0,0 @@
export type FinalFunction = {
folder: string;
funcs: Func[];
nestedLevel: number;
parentFolder?: string;
};
export type Func = {
name: string;
id: string;
lang: string;
functionName: string;
route?: string;
path?: string;
createdAt?: string;
updatedAt?: string;
createdWithCommitSha?: string;
formattedCreatedAt?: string;
formattedUpdatedAt?: string;
};
export const normalizeFunctionMetadata = (functions): FinalFunction[] => {
const finalFunctions: FinalFunction[] = [
{ folder: 'functions', funcs: [], nestedLevel: 0 },
];
const topLevelFunctionsFolder = finalFunctions[0].funcs;
functions.forEach((func) => {
const nestedLevel = func.path?.split('/').length;
const newFuncToAdd = {
...func,
name: func.path?.split('/')[nestedLevel - 1],
lang: func.path?.split('.')[1],
// formattedCreatedAt: `${format(
// parseISO(func.createdAt),
// 'yyyy-MM-dd HH:mm:ss',
// )}`,
// formattedUpdatedAt: `${formatDistanceToNowStrict(
// parseISO(func.updatedAt),
// {
// addSuffix: true,
// },
// )}`,
};
if (nestedLevel === 2) {
topLevelFunctionsFolder.push(newFuncToAdd);
} else if (nestedLevel > 2) {
const nameOfTheFolder = func.path?.split('/')[nestedLevel - 2];
const nameOfParentFolder = func.path?.split('/')[nestedLevel - 3];
const checkForFolderExistence = finalFunctions.find(
(functionFolder) => functionFolder.folder === nameOfTheFolder,
);
if (!checkForFolderExistence) {
finalFunctions.push({
folder: nameOfTheFolder,
funcs: [newFuncToAdd],
nestedLevel: nestedLevel - 2,
parentFolder: nameOfParentFolder,
});
} else {
checkForFolderExistence.funcs.push(newFuncToAdd);
}
}
});
// Sort folders by putting the subfolder next to their parent folder, even though they share the same place in the array
// except for the nestedLevel prop. A future change to this would be to make folders have subfolders, which is easier
// understand, but would require a change in the UI.
// @TODO: Change to have elements have subfolders inside the object?
finalFunctions.sort((a, b) => {
if (a.folder === b.parentFolder) {
return -1;
}
return 1;
});
return finalFunctions;
};

View File

@@ -1,39 +0,0 @@
import { format, parseISO } from 'date-fns';
import type { FunctionLog } from './FunctionLog';
import type { FunctionResponseLog } from './FunctionResponseLog';
export const normalizeToIndividualFunctionsWithLogs = (
functionLogs: FunctionResponseLog[],
) => {
const arrayOfFunctions: FunctionLog[] = [];
const sortedFunctions = [...functionLogs].sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
sortedFunctions.forEach((functionLog) => {
const funcName = functionLog.functionPath;
const logMessage = {
createdAt: functionLog.createdAt,
date: `${format(parseISO(functionLog.createdAt), 'yyyy-MM-dd HH:mm:ss')}`,
message: functionLog.message,
};
const newFunc = {
name: funcName,
language: functionLog.functionPath.split('.')[1],
logs: [logMessage],
};
// If the function is already in the array of functions to log, just add the new log message to the existing object...
if (arrayOfFunctions.some((obj) => obj.name === funcName)) {
const index = arrayOfFunctions.findIndex((obj) => obj.name === funcName);
const currentFunction = arrayOfFunctions[index];
currentFunction.logs.push(logMessage);
} else {
// If the function is not in the array of functions, add it with the log message to it.
arrayOfFunctions.push(newFunc);
}
});
return arrayOfFunctions;
};
export default normalizeToIndividualFunctionsWithLogs;

View File

@@ -4,7 +4,17 @@ import * as Yup from 'yup';
const ruleSchema = Yup.object().shape({
column: Yup.string().nullable().required('Please select a column.'),
operator: Yup.string().nullable().required('Please select an operator.'),
value: Yup.string().nullable().required('Please enter a value.'),
value: Yup.mixed()
.test(
'isArray',
'Please enter a valid value.',
(value) =>
typeof value === 'string' ||
(Array.isArray(value) &&
value.every((item) => typeof item === 'string')),
)
.nullable()
.required('Please enter a value.'),
});
const ruleGroupSchema = Yup.object().shape({

View File

@@ -37,7 +37,7 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
parseInt(router.query.page as string, 10) - 1 || 0,
);
const [sortBy, setSortBy] = useState<SortingRule<StoredFile>[]>();
const limit = 25;
const limit = 10;
const emptyStateMessage = searchString
? 'No search results found.'
: 'No files are uploaded yet.';

View File

@@ -5,12 +5,16 @@ import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAn
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { useState } from 'react';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface AllowedEmailSettingsFormValues {
/**
* Determines whether or not the allowed email settings are enabled.
*/
enabled: boolean;
/**
* Set of email that are allowed to be used for project's users authentication.
*/
@@ -25,7 +29,6 @@ export interface AllowedEmailSettingsFormValues {
export default function AllowedEmailDomainsSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const [enabled, setEnabled] = useState(false);
const { data, loading, error } = useGetAppQuery({
variables: {
@@ -36,12 +39,30 @@ export default function AllowedEmailDomainsSettings() {
const form = useForm<AllowedEmailSettingsFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
enabled:
Boolean(data?.app?.authAccessControlAllowedEmails) ||
Boolean(data?.app?.authAccessControlAllowedEmailDomains),
authAccessControlAllowedEmails: data?.app?.authAccessControlAllowedEmails,
authAccessControlAllowedEmailDomains:
data?.app?.authAccessControlAllowedEmailDomains,
},
});
const { register, formState, setValue, watch } = form;
const enabled = watch('enabled');
const isDirty = Object.keys(formState.dirtyFields).length > 0;
useEffect(() => {
if (
!data.app?.authAccessControlAllowedEmails &&
!data.app?.authAccessControlAllowedEmailDomains
) {
return;
}
setValue('enabled', true, { shouldDirty: false });
}, [data.app, setValue]);
if (loading) {
return (
<ActivityIndicator
@@ -56,8 +77,6 @@ export default function AllowedEmailDomainsSettings() {
throw error;
}
const { register, formState } = form;
const handleAllowedEmailDomainsChange = async (
values: AllowedEmailSettingsFormValues,
) => {
@@ -65,7 +84,12 @@ export default function AllowedEmailDomainsSettings() {
variables: {
id: currentApplication.id,
app: {
...values,
authAccessControlAllowedEmails: values.enabled
? values.authAccessControlAllowedEmails
: '',
authAccessControlAllowedEmailDomains: values.enabled
? values.authAccessControlAllowedEmailDomains
: '',
},
},
});
@@ -89,13 +113,17 @@ export default function AllowedEmailDomainsSettings() {
<SettingsContainer
title="Allowed Emails and Domains"
description="Allow specific email addresses and domains to sign up."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
slotProps={{
submitButton: {
disabled: !formState.isValid || !isDirty,
loading: formState.isSubmitting,
},
}}
docsLink="https://docs.nhost.io/platform/authentication"
enabled={enabled}
onEnabledChange={setEnabled}
onEnabledChange={(switchEnabled) =>
setValue('enabled', switchEnabled, { shouldDirty: true })
}
showSwitch
className={twMerge(
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',

View File

@@ -5,12 +5,16 @@ import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAn
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import Input from '@/ui/v2/Input';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { useState } from 'react';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { twMerge } from 'tailwind-merge';
export interface BlockedEmailFormValues {
/**
* Determines whether or not the blocked email settings are enabled.
*/
enabled: boolean;
/**
* Set of emails that are blocked from registering to the user's project.
*/
@@ -24,7 +28,6 @@ export interface BlockedEmailFormValues {
export default function BlockedEmailSettings() {
const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApp] = useUpdateAppMutation();
const [enabled, setEnabled] = useState(false);
const { data, loading, error } = useGetAppQuery({
variables: {
@@ -35,12 +38,30 @@ export default function BlockedEmailSettings() {
const form = useForm<BlockedEmailFormValues>({
reValidateMode: 'onSubmit',
defaultValues: {
enabled:
Boolean(data?.app?.authAccessControlBlockedEmails) ||
Boolean(data?.app?.authAccessControlBlockedEmailDomains),
authAccessControlBlockedEmails: data?.app?.authAccessControlBlockedEmails,
authAccessControlBlockedEmailDomains:
data?.app?.authAccessControlBlockedEmailDomains,
},
});
const { register, formState, setValue, watch } = form;
const enabled = watch('enabled');
const isDirty = Object.keys(formState.dirtyFields).length > 0;
useEffect(() => {
if (
!data.app?.authAccessControlBlockedEmails &&
!data.app?.authAccessControlBlockedEmailDomains
) {
return;
}
setValue('enabled', true, { shouldDirty: false });
}, [data.app, setValue]);
if (loading) {
return (
<ActivityIndicator
@@ -55,8 +76,6 @@ export default function BlockedEmailSettings() {
throw error;
}
const { register, formState } = form;
const handleAllowedEmailDomainsChange = async (
values: BlockedEmailFormValues,
) => {
@@ -64,7 +83,12 @@ export default function BlockedEmailSettings() {
variables: {
id: currentApplication.id,
app: {
...values,
authAccessControlBlockedEmails: values.enabled
? values.authAccessControlBlockedEmails
: '',
authAccessControlBlockedEmailDomains: values.enabled
? values.authAccessControlBlockedEmailDomains
: '',
},
},
});
@@ -88,13 +112,17 @@ export default function BlockedEmailSettings() {
<SettingsContainer
title="Blocked Emails and Domains"
description="Block specific email addresses and domains to sign up."
primaryActionButtonProps={{
disabled: !formState.isValid || !formState.isDirty,
loading: formState.isSubmitting,
slotProps={{
submitButton: {
disabled: !formState.isValid || !isDirty,
loading: formState.isSubmitting,
},
}}
docsLink="https://docs.nhost.io/platform/authentication"
enabled={enabled}
onEnabledChange={setEnabled}
onEnabledChange={(switchEnabled) =>
setValue('enabled', switchEnabled, { shouldDirty: true })
}
showSwitch
className={twMerge(
'row-span-2 grid grid-flow-row gap-4 px-4 lg:grid-cols-3',

View File

@@ -67,8 +67,8 @@ export default function CreateUserForm({
} = form;
const baseAuthUrl = generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
currentApplication?.subdomain,
currentApplication?.region?.awsName,
'auth',
);

View File

@@ -8,18 +8,18 @@ import Button from '@/ui/v2/Button';
import Chip from '@/ui/v2/Chip';
import { Dropdown } from '@/ui/v2/Dropdown';
import IconButton from '@/ui/v2/IconButton';
import CopyIcon from '@/ui/v2/icons/CopyIcon';
import Input from '@/ui/v2/Input';
import InputLabel from '@/ui/v2/InputLabel';
import Option from '@/ui/v2/Option';
import Text from '@/ui/v2/Text';
import CopyIcon from '@/ui/v2/icons/CopyIcon';
import { copy } from '@/utils/copy';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import {
useGetRolesQuery,
useUpdateRemoteAppUserMutation,
} from '@/utils/__generated__/graphql';
import { copy } from '@/utils/copy';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import { yupResolver } from '@hookform/resolvers/yup';
import { Avatar } from '@mui/material';
import { format } from 'date-fns';
@@ -137,7 +137,7 @@ export default function EditUserForm({
}
const { data: dataRoles } = useGetRolesQuery({
variables: { id: currentApplication.id },
variables: { id: currentApplication?.id },
});
const allAvailableProjectRoles = getUserRoles(
@@ -206,11 +206,7 @@ export default function EditUserForm({
</div>
<div>
<Dropdown.Root>
<Dropdown.Trigger
autoFocus={false}
asChild
className="gap-2"
>
<Dropdown.Trigger autoFocus={false} asChild className="gap-2">
<Button variant="outlined" color="secondary">
Actions
</Button>

View File

@@ -7,12 +7,14 @@ import Chip from '@/ui/v2/Chip';
import Divider from '@/ui/v2/Divider';
import { Dropdown } from '@/ui/v2/Dropdown';
import IconButton from '@/ui/v2/IconButton';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text';
import DotsHorizontalIcon from '@/ui/v2/icons/DotsHorizontalIcon';
import TrashIcon from '@/ui/v2/icons/TrashIcon';
import UserIcon from '@/ui/v2/icons/UserIcon';
import List from '@/ui/v2/List';
import { ListItem } from '@/ui/v2/ListItem';
import Text from '@/ui/v2/Text';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
import {
useDeleteRemoteAppUserRolesMutation,
@@ -21,9 +23,6 @@ import {
useRemoteAppDeleteUserMutation,
useUpdateRemoteAppUserMutation,
} from '@/utils/__generated__/graphql';
import getUserRoles from '@/utils/settings/getUserRoles';
import { toastStyleProps } from '@/utils/settings/settingsConstants';
import type { ApolloQueryResult } from '@apollo/client';
import { Avatar } from '@mui/material';
import { formatDistance } from 'date-fns';
@@ -77,7 +76,7 @@ export default function UsersBody({
* in the drawer form.
*/
const { data: dataRoles } = useGetRolesQuery({
variables: { id: currentApplication.id },
variables: { id: currentApplication?.id },
});
const allAvailableProjectRoles = useMemo(

View File

@@ -1,114 +0,0 @@
const terminalTheme = {
hljs: {
display: 'block',
background: '#F4F7F9',
color: '#21324B',
},
'hljs-tag': {
color: '#9C73DF',
},
'hljs-keyword': {
color: '#9C73DF',
fontWeight: 'bold',
},
'hljs-selector-tag': {
color: '#9C73DF',
fontWeight: 'bold',
},
'hljs-literal': {
color: '#9C73DF',
fontWeight: 'bold',
},
'hljs-strong': {
color: '#9C73DF',
},
'hljs-name': {
color: '#9C73DF',
},
'hljs-code': {
color: '#66d9ef',
},
'hljs-class .hljs-title': {
color: 'red',
},
'hljs-attribute': {
color: '#bf79db',
},
'hljs-symbol': {
color: '#bf79db',
},
'hljs-regexp': {
color: '#bf79db',
},
'hljs-link': {
color: '#bf79db',
},
'hljs-string': {
color: '#B7A590',
},
'hljs-bullet': {
color: '#B7A590',
},
'hljs-subst': {
color: '#B7A590',
},
'hljs-title': {
color: '#B7A590',
fontWeight: 'bold',
},
'hljs-section': {
color: '#B7A590',
fontWeight: 'bold',
},
'hljs-emphasis': {
color: '#3ECF8E',
},
'hljs-type': {
color: '#3ECF8E',
fontWeight: 'bold',
},
'hljs-built_in': {
color: '#3ECF8E',
},
'hljs-builtin-name': {
color: '#3ECF8E',
},
'hljs-selector-attr': {
color: '#3ECF8E',
},
'hljs-selector-pseudo': {
color: '#3ECF8E',
},
'hljs-addition': {
color: '#3ECF8E',
},
'hljs-variable': {
color: '#3ECF8E',
},
'hljs-template-tag': {
color: '#3ECF8E',
},
'hljs-template-variable': {
color: '#3ECF8E',
},
'hljs-comment': {
color: '#21324B',
},
'hljs-quote': {
color: '#75715e',
},
'hljs-deletion': {
color: '#75715e',
},
'hljs-meta': {
color: '#75715e',
},
'hljs-doctag': {
fontWeight: 'bold',
},
'hljs-selector-id': {
fontWeight: 'bold',
},
};
export default terminalTheme;

View File

@@ -6,7 +6,6 @@ import FileTextIcon from '@/ui/v2/icons/FileTextIcon';
import GraphQLIcon from '@/ui/v2/icons/GraphQLIcon';
import HasuraIcon from '@/ui/v2/icons/HasuraIcon';
import HomeIcon from '@/ui/v2/icons/HomeIcon';
import LambdaIcon from '@/ui/v2/icons/LambdaIcon';
import RocketIcon from '@/ui/v2/icons/RocketIcon';
import StorageIcon from '@/ui/v2/icons/StorageIcon';
import UserIcon from '@/ui/v2/icons/UserIcon';
@@ -54,13 +53,6 @@ export default function useProjectRoutes() {
const isPlatform = useIsPlatform();
const nhostRoutes: ProjectRoute[] = [
{
relativePath: '/functions',
exact: false,
label: 'Functions',
icon: <LambdaIcon />,
disabled: !isPlatform,
},
{
relativePath: '/deployments',
exact: false,

View File

@@ -70,7 +70,7 @@ export default function useFiles({
currentApplication.subdomain,
currentApplication.region.awsName,
'storage',
)}/${file.id}`;
)}/files/${file.id}`;
const fetchParams = new URLSearchParams();

View File

@@ -1,269 +0,0 @@
import ConnectGithubModal from '@/components/applications/ConnectGithubModal';
import { FunctionsNotDeployed } from '@/components/applications/functions/FunctionsNotDeployed';
import { normalizeFunctionMetadata } from '@/components/applications/functions/normalizeFunctionMetadata';
import { EditRepositorySettings } from '@/components/applications/github/EditRepositorySettings';
import useGitHubModal from '@/components/applications/github/useGitHubModal';
import Folder from '@/components/icons/Folder';
import Container from '@/components/layout/Container';
import ProjectLayout from '@/components/layout/ProjectLayout';
import { useWorkspaceContext } from '@/context/workspace-context';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { Button } from '@/ui/Button';
import DelayedLoading from '@/ui/DelayedLoading';
import { Modal } from '@/ui/Modal';
import Status, { StatusEnum } from '@/ui/Status';
import { Text } from '@/ui/Text';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { useGetAppFunctionsMetadataQuery } from '@/utils/__generated__/graphql';
import { ChevronRightIcon } from '@heroicons/react/solid';
import clsx from 'clsx';
import Image from 'next/image';
import Link from 'next/link';
import type { ReactElement } from 'react';
import { useEffect, useState } from 'react';
function FunctionsNoRepo() {
const [githubModal, setGithubModal] = useState(false);
const [githubRepoModal, setGithubRepoModal] = useState(false);
const { openGitHubModal } = useGitHubModal();
return (
<>
<Modal showModal={githubModal} close={() => setGithubModal(!githubModal)}>
<ConnectGithubModal close={() => setGithubModal(false)} />
</Modal>
<Modal
showModal={githubRepoModal}
close={() => setGithubRepoModal(!githubRepoModal)}
>
<EditRepositorySettings
openConnectGithubModal={() => setGithubModal(true)}
close={() => setGithubRepoModal(false)}
handleSelectAnotherRepository={openGitHubModal}
/>
</Modal>
<div className="mx-auto flex w-centImage flex-col text-center">
<Image
src="/assets/githubRepo.svg"
width={72}
height={72}
alt="GitHub Logo"
/>
</div>
<Text className="mt-4 font-medium" size="large" color="dark">
Function Logs
</Text>
<div className="flex">
<div className="mx-auto flex flex-row self-center text-center">
<Text size="normal" color="greyscaleDark" className="mt-1">
To deploy serverless functions, you need to connect your project to
version control.
</Text>
</div>
</div>
<div className="mt-3 flex text-center">
<Button
transparent
color="blue"
className="mx-auto font-medium"
onClick={() => setGithubModal(true)}
>
Connect your Project to GitHub
</Button>
</div>
</>
);
}
export default function FunctionsPage() {
const { currentWorkspace, currentApplication } =
useCurrentWorkspaceAndApplication();
const { workspaceContext } = useWorkspaceContext();
const { data, loading, error } = useGetAppFunctionsMetadataQuery({
variables: { id: currentApplication?.id },
});
const [normalizedFunctions, setNormalizedFunctions] = useState(null);
useEffect(() => {
if (!data) {
return;
}
if (data.app.metadataFunctions) {
setNormalizedFunctions(
normalizeFunctionMetadata(data.app.metadataFunctions),
);
}
}, [data]);
if (!workspaceContext.repository) {
return (
<Container className="mt-12 max-w-3xl text-center antialiased">
<FunctionsNoRepo />
</Container>
);
}
if (loading) {
return (
<Container>
<DelayedLoading delay={500} className="mt-12" />
</Container>
);
}
if (!data || normalizedFunctions === null) {
return (
<Container>
<FunctionsNotDeployed />
</Container>
);
}
if (error) {
return <Container>Error</Container>;
}
return (
<Container>
<div className="mt-2">
{normalizedFunctions?.map((folder) => (
<div key={folder.folder}>
<div
className={clsx(
'flex flex-row pt-8 pb-2 align-middle',
folder.nestedLevel < 2 && 'ml-6',
folder.nestedLevel >= 2 && 'ml-12',
)}
>
<div className={clsx('flex w-full')}>
{folder.nestedLevel > 0 && (
<Folder className="self-center align-middle text-greyscaleGrey" />
)}
<Text
color="greyscaleDark"
variant="body"
className={clsx(
'font-medium',
folder.nestedLevel > 0 && 'ml-2',
)}
size="tiny"
>
{folder.folder}/
</Text>
</div>
{folder.nestedLevel === 0 ? (
<div className="flex w-full flex-row">
<div className="flex w-52">
<Text
color="greyscaleDark"
variant="body"
className="font-medium"
size="tiny"
>
Created At
</Text>
</div>
<div className="flex w-16 self-end">
<Text
color="greyscaleDark"
variant="body"
className="font-medium"
size="tiny"
>
Status
</Text>
</div>
</div>
) : null}
</div>
<div
className={clsx(
'border-t py-1',
folder.nestedLevel < 2 && 'ml-6',
folder.nestedLevel >= 2 && 'ml-12',
)}
>
{folder.funcs.map((func) => (
<Link
key={func.id}
href={{
pathname:
'/[workspaceSlug]/[appSlug]/functions/[functionId]',
query: {
workspaceSlug: currentWorkspace.slug,
appSlug: currentApplication.slug,
functionId: func.functionName,
},
}}
passHref
>
<a
href="[workspaceSlug]/[appSlug]/functions/[functionId]"
className={clsx(
'flex cursor-pointer flex-row border-b py-2.5',
folder.nestedLevel && 'ml-0',
)}
>
<div className="flex w-full flex-row items-center">
<Image
src={`/assets/functions/${func.lang}.svg`}
alt={`Logo of ${func.lang}`}
width={16}
height={16}
/>
<Text
color="greyscaleDark"
variant="body"
className="pl-2 font-medium"
size="small"
>
{func.name}
</Text>
</div>
<div className="flex w-full flex-row">
<div className={clsx('flex w-52 self-center')}>
<Text
color="greyscaleDark"
variant="body"
className=""
size="tiny"
>
{func.formattedCreatedAt || '-'}
</Text>
</div>
<div className="flex w-16 self-center">
<Status status={StatusEnum.Live}>Live</Status>
<ChevronRightIcon className="middl ml-2 h-4 w-4 cursor-pointer self-center" />
</div>
</div>
</a>
</Link>
))}
</div>
</div>
))}
</div>
<div className="mx-auto mt-10 max-w-6xl">
<div className="text-center">
<Text size="tiny" color="greyscaleDark" className="font-medium">
Base URL for function endpoints is{' '}
{generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
'functions',
)}
</Text>
</div>
</div>
</Container>
);
}
FunctionsPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
};

View File

@@ -1,142 +0,0 @@
import { FunctionsLogsTerminalPage } from '@/components/applications/functions/FunctionLogsTerminalFromPage';
import type { Func } from '@/components/applications/functions/normalizeFunctionMetadata';
import { normalizeFunctionMetadata } from '@/components/applications/functions/normalizeFunctionMetadata';
import { LoadingScreen } from '@/components/common/LoadingScreen';
import Help from '@/components/icons/Help';
import Container from '@/components/layout/Container';
import ProjectLayout from '@/components/layout/ProjectLayout';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useGetAllUserWorkspacesAndApplications } from '@/hooks/useGetAllUserWorkspacesAndApplications';
import { Text } from '@/ui/Text';
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { yieldFunction } from '@/utils/helpers';
import { useGetAppFunctionsMetadataQuery } from '@/utils/__generated__/graphql';
import Image from 'next/image';
import { useRouter } from 'next/router';
import type { ReactElement } from 'react';
import { useEffect, useState } from 'react';
export default function FunctionDetailsPage() {
const { currentApplication, currentWorkspace } =
useCurrentWorkspaceAndApplication();
useGetAllUserWorkspacesAndApplications(false);
const { data, loading, error } = useGetAppFunctionsMetadataQuery({
variables: { id: currentApplication?.id },
});
const [currentFunction, setCurrentFunction] = useState<Func | null>(null);
const router = useRouter();
// currentFunction will be null until we get data back from remote and we set it to be the function we're looking for.
useEffect(() => {
if (!data) {
return;
}
const appFunctions = normalizeFunctionMetadata(data?.app.metadataFunctions);
setCurrentFunction(yieldFunction(appFunctions, router));
}, [data, router]);
if (!currentApplication || !currentWorkspace || loading) {
return <LoadingScreen />;
}
if (error) {
throw new Error(
error.message ||
'An unexpected error has ocurred. Please try again later.',
);
}
if (!currentFunction) {
return (
<Container>
<h1 className="text-4xl font-semibold text-greyscaleDark">Not found</h1>
<p className="text-sm text-greyscaleGrey">
This function does not exist.
</p>
</Container>
);
}
return (
<>
<Container>
<div className="flex place-content-between">
<div className="flex flex-row items-center py-1">
<Image
src={`/assets/functions/${
currentFunction.name.split('.')[1]
}.svg`}
alt={`Logo of ${currentFunction.name.split('.')[1]}`}
width={40}
height={40}
/>
<div className="flex flex-col">
<Text
color="greyscaleDark"
variant="body"
className="ml-2 font-medium"
size="big"
>
{currentFunction.name}
</Text>
<a
className="ml-2 text-xs font-medium text-greyscaleGrey"
href={`${generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
'functions',
)}${currentFunction?.route}`}
target="_blank"
rel="noreferrer"
>
{`${generateAppServiceUrl(
currentApplication.subdomain,
currentApplication.region.awsName,
'functions',
)}${currentFunction?.route}`}
</a>
</div>
</div>
</div>
</Container>
<Container className="pt-10">
<div className="flex flex-row place-content-between">
<div className="flex">
<Text size="large" className="font-medium" color="greyscaleDark">
Log
</Text>
</div>
<div className="flex">
<Text
size="tiny"
className="self-center font-medium"
color="greyscaleDark"
>
Awaiting new requests
</Text>
<a
href="https://docs.nhost.io/platform/serverless-functions"
target="_blank"
rel="noreferrer"
>
<Help className="h-7 w-7" />
</a>
</div>
</div>
<div className="mt-5">
<FunctionsLogsTerminalPage functionName={currentFunction?.path} />
</div>
</Container>
</>
);
}
FunctionDetailsPage.getLayout = function getLayout(page: ReactElement) {
return <ProjectLayout>{page}</ProjectLayout>;
};

View File

@@ -1,10 +1,5 @@
import type {
FinalFunction,
Func,
} from '@/components/applications/functions/normalizeFunctionMetadata';
import features from '@/data/features.json';
import { ApplicationStatus } from '@/types/application';
import type { NextRouter } from 'next/router';
import slugify from 'slugify';
import { LOCAL_BACKEND_URL } from './env';
import type { DeploymentRowFragment } from './__generated__/graphql';
@@ -89,26 +84,6 @@ export function emptyWorkspace() {
};
}
export function yieldFunction(
functionsToSearch: FinalFunction[],
router: NextRouter,
): Func {
let functionToReturn: Func = null;
functionsToSearch.forEach((currentFolder) => {
currentFolder.funcs.forEach((currentFunction) => {
if (
!functionToReturn &&
currentFunction.functionName === router.query.functionId
) {
functionToReturn = currentFunction;
}
});
});
return functionToReturn;
}
/**
* Converts the state number of the application to its string equivalent.
* @param appStatus The current state of the application.

View File

@@ -1,5 +1,11 @@
# @nhost/docs
## 0.0.11
### Patch Changes
- e6dad4d6: Added remote schemas
## 0.0.10
### Patch Changes

View File

@@ -0,0 +1,4 @@
{
"label": "Remote Schemas",
"position": 11
}

View File

@@ -0,0 +1,252 @@
---
title: Stripe GraphQL API
sidebar_label: Stripe
sidebar_position: 2
image: /img/og/graphql.png
---
import Tabs from '@theme/Tabs'
import TabItem from '@theme/TabItem'
This package creates a Stripe GraphQL API, allowing for interaction with data at Stripe.
Here's an example of how to use the Stripe GraphQL API to get a list of invoices for a specific Stripe customer:
<Tabs >
<TabItem value="request" label="Request" default>
```graphql
query {
stripe {
customer(id: "cus_xxx") {
id
name
invoices {
data {
id
created
paid
hostedInvoiceUrl
}
}
}
}
}
```
</TabItem>
<TabItem value="response" label="Response">
```json
{
"data": {
"stripe": {
"customer": {
"id": "cus_xxx",
"name": "joe@example.com",
"invoices": {
"data": [
{
"id": "in_1MUmwnCCF9wuB4xxxxxxxx",
"created": 1674806769,
"paid": true,
"hostedInvoiceUrl": "https://invoice.stripe.com/i/acct_xxxxxxx/test_YWNjdF8xS25xV1lDQ0Y5d3VCNGZYLF9ORkhWxxxxxxxxxxxx?s=ap"
}
]
}
}
}
}
}
```
</TabItem>
</Tabs>
It's recommended to add the Stripe GraphQL API as a [Remote Schema in Hasura](https://hasura.io/docs/latest/remote-schemas/index/) and connect data from your database with data in Stripe. By doing so, it's possible to request data from your database and Stripe in a single GraphQL query.
Here's an example of how to use the Stripe GraphQL API to get a list of invoices for a specific Stripe customer. Note that the user data is fetched from your database and the Stripe customer data is fetched from Stripe:
```graphql
query {
users {
# User in your database
id
displayName
userData {
stripeCustomerId # Customer's Stripe Customer Id
stripeCustomer {
# Data from Stripe
id
name
paymentMethods {
id
card {
brand
last4
}
}
}
}
}
}
```
## Get Started
Install the package:
<Tabs groupId="package-manager">
<TabItem value="npm" label="npm" default>
```bash
npm install @nhost/stripe-graphql-js
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```bash
yarn install @nhost/stripe-graphql-js
```
</TabItem>
<TabItem value="pnpm" label="pnpm">
```bash
pnpm add @nhost/stripe-graphql-js
```
</TabItem>
</Tabs>
## Serverless Function
Create a new [Serverless Function](/serverless-functions): `functions/graphql/stripe.ts`:
```ts
import { createStripeGraphQLServer } from '@nhost/stripe-graphql-js'
const server = createStripeGraphQLServer()
export default server
```
> You can run the Stripe GraphQL API in any Node.js environment because it's built using [GraphQL Yoga](https://github.com/dotansimha/graphql-yoga).
## Stripe Secret Key
Add `STRIPE_SECRET_KEY` as an environment variable.
If you're using Nhost, add `STRIPE_SECRET_KEY` to `.env.development` like this:
```
STRIPE_SECRET_KEY=sk_test_***
```
And add the production key (`sk_live_***`) to [environment variables](/platform/environment-variables) in the Nhost dashboard.
Learn more about [Stripe API keys](https://stripe.com/docs/keys#obtain-api-keys).
## Start Nhost
```
nhost up
```
Learn more about the [Nhost CLI](/cli).
## Test
Test the Stripe GraphQL API in the browser:
[http://localhost:1337/v1/functions/graphql/stripe](http://localhost:1337/v1/functions/graphql/stripe)
## Remote Schema
Add the Stripe GraphQL API as a Remote Schema in Hasura.
**URL**
```
{{NHOST_FUNCTIONS_URL}}/graphql/stripe
```
**Headers**
```
x-nhost-webhook-secret: NHOST_WEBHOOK_SECRET (From env var)
```
> The `NHOST_WEBHOOK_SECRET` is used to verify that the request is coming from Nhost. The environment variable is a [system environment variable](/platform/environment-variables#system-environment-variables) and is always available.
![Hasura Remote Schema](/img/graphql/remote-schemas/stripe/remote-schema.png)
## Permissions
Here's a minimal example without any custom permissions. Only requests using the `x-hasura-admin-secret` header will work:
```js
const server = createStripeGraphQLServer()
```
For more granular permissions, you can pass an `isAllowed` function to the `createStripeGraphQLServer`. The `isAllowed` function takes a `stripeCustomerId` and [`context`](#context) as parameters and runs every time the GraphQL server makes a request to Stripe to get or modify data for a specific Stripe customer.
Here is an example of an `isAllowed` function:
```ts
import { createStripeGraphQLServer } from '@nhost/stripe-graphql-js'
const isAllowed = (stripeCustomerId: string, context: Context) => {
const { isAdmin, userClaims } = context
// allow all requests if they have a valid `x-hasura-admin-secret`
if (isAdmin) {
return true
}
// get user id
const userId = userClaims['x-hasura-user-id']
// check if the user is signed in
if (!userId) {
return false
}
// get more user information from the database
const { user } = await gqlSDK.getUser({
id: userId
})
if (!user) {
return false
}
// check if the user is part of a workspace with the `stripeCustomerId`
return user.workspaceMembers.some((workspaceMember) => {
return workspaceMember.workspace.stripeCustomerId === stripeCustomerId
})
}
const server = createStripeGraphQLServer({ isAllowed })
export default server
```
### Context
The `context` object contains:
- `userClaims` - verified JWT claims from the user's access token.
- `isAdmin` - `true` if the request was made using a valid `x-hasura-admin-secret` header.
- `request` - [Fetch API Request object](https://developer.mozilla.org/en-US/docs/Web/API/Request) that represents the incoming HTTP request in platform-independent way. It can be useful for accessing headers to authenticate a user
- `query` - the DocumentNode that was parsed from the GraphQL query string
- `operationName` - the operation name selected from the incoming query
- `variables` - the variables that were defined in the query
- `extensions` - the extensions that were received from the client
Read more about the [default context from GraphQL Yoga](https://www.the-guild.dev/graphql/yoga-server/docs/features/context#default-context).
## Source Code
The source code is available on [GitHub](https://github.com/nhost/nhost/tree/main/integrations/stripe-graphql-js).

View File

@@ -11,9 +11,9 @@ With Nhost, you can deploy Serverless Functions to execute custom code. Each Ser
Serverless functions can be used to handle [event triggers](/database/event-triggers), form submissions, integrations (e.g. Stripe, Slack, etc), and more.
## Creating a Serverless Function
## Create a Serverless Function
Every `.js` (JavaScript) and `.ts` (TypeScript) file in the `functions/` folder of your Nhost project is its own Serverless Function.
Every `.ts` (TypeScript) and `.js` (JavaScript) file in the `functions/` folder of your Nhost project is its own Serverless Function.
<Tabs groupId="language">
<TabItem value="ts" label="TypeScript" default>

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/docs",
"version": "0.0.10",
"version": "0.0.11",
"private": true,
"scripts": {
"docusaurus": "docusaurus",

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

View File

@@ -1,5 +1,12 @@
# @nhost-examples/serverless-functions
## 0.0.5
### Patch Changes
- Updated dependencies [e6dad4d6]
- @nhost/stripe-graphql-js@1.0.0
## 0.0.4
### Patch Changes

View File

@@ -1,13 +1,13 @@
{
"name": "@nhost-examples/serverless-functions",
"private": true,
"version": "0.0.4",
"version": "0.0.5",
"devDependencies": {
"@types/express": "^4.17.13"
},
"dependencies": {
"@graphql-yoga/node": "^2.13.13",
"@nhost/stripe-graphql-js": "^0.0.8",
"@nhost/stripe-graphql-js": "^1.0.0",
"@pothos/core": "^3.21.0",
"cross-fetch": "^3.1.5",
"graphql": "15.7.2",

View File

@@ -1,5 +1,11 @@
# @nhost/stripe-graphql-js
## 1.0.0
### Major Changes
- e6dad4d6: Added remote schemas
## 0.0.8
### Patch Changes

View File

@@ -1,5 +1,5 @@
<h1 align="center">@nhost/stripe-graphql-js</h1>
<h2 align="center">Stripe GraphQL API</h2>
<h1 align="center">Stripe GraphQL API</h1>
<h2 align="center">@nhost/stripe-graphql-js</h2>
<p align="center">
<img alt="npm" src="https://img.shields.io/npm/v/@nhost/stripe-graphql-js">
@@ -9,179 +9,9 @@
</a>
</p>
This package creates a Stripe GraphQL API.
## Documentation
```graphql
query {
stripe {
customer(id: "cus_xxx" {
id
name
invoices {
data {
id
created
paid
hostedInvoiceUrl
}
}
}
}
}
```
You can also add the Stripe GraphQL API as a Hasura Remote Schema and connect data from your database and Stripe. This allows you to request data from your database and Stripe in a single GraphQL query:
```graphql
query {
users {
# User in your database
id
displayName
userData {
stripeCustomerId # Customer's Stripe Customer Id
stripeCustomer {
# Data from Stripe
id
name
paymentMethods {
id
card {
brand
last4
}
}
}
}
}
}
```
## Install
```bash
npm install @nhost/stripe-graphql-js
```
## Quick Start
### Serverless Function Setup
Create a new [Serverless Function](https://docs.nhost.io/platform/serverless-functions) `functions/graphql/stripe.ts`:
```js
import { createStripeGraphQLServer } from '@nhost/stripe-graphql-js'
const server = createStripeGraphQLServer()
export default server
```
> You can run the Stripe GraphQL API in any JS environment because it's built using [GraphQL Yoga](https://github.com/dotansimha/graphql-yoga).
### Stripe Secret Key
Add `STRIPE_SECRET_KEY` as an environment variable. If you're using Nhost, add `STRIPE_SECRET_KEY` to `.env.development` like this:
```
STRIPE_SECRET_KEY=sk_test_xxx
```
Learn more about [Stripe API keys](https://stripe.com/docs/keys#obtain-api-keys).
### Start Nhost
```
nhost up
```
Learn more about the [Nhost CLI](https://docs.nhost.io/platform/cli).
### Test
Test the Stripe GraphQL API in the browser:
[http://localhost:1337/v1/functions/graphql/stripe](http://localhost:1337/v1/functions/graphql/stripe)
### Remote Schema
Add the Stripe GraphQL API as a Remote Schema in Hasura.
**URL**
```
{{NHOST_BACKEND_URL}}/v1/functions/graphql/stripe
```
**Headers**
```
x-nhost-webhook-secret: NHOST_WEBHOOK_SECRET (from env var)
```
![Hasura Remote Schema](./assets//hasura-remote-schema.png)
## Permissions
Here's a minimal example without any custom permissions. Only requests using the `x-hasura-admin-secret` header will work:
```js
const server = createStripeGraphQLServer()
```
For more granular permissions, you can pass an `isAllowed` function to the `createStripeGraphQLServer`. The `isAllowed` function takes a `stripeCustomerId` and [`context`](#context) as parameters and runs every time the GraphQL server makes a request to Stripe to get or modify data for a specific Stripe customer.
Here is an example of an `isAllowed` function:
```js
const isAllowed = (stripeCustomerId: string, context: Context) => {
const { isAdmin, userClaims } = context
// allow requests if it has a valid `x-hasura-admin-secret`
if (isAdmin) {
return true
}
// get user id
const userId = userClaims['x-hasura-user-id']
// check if user is signed in
if (!userId) {
return false;
}
// get more user information from the database
const { user } = await gqlSDK.getUser({
id: userId,
});
if (!user) {
return false;
}
// check if the user is part of a workspace with the `stripeCustomerId`
return user.workspaceMembers
.some((workspaceMember) => {
return workspaceMember.workspace.stripeCustomerId === stripeCustomerId;
});
}
```
### Context
The `context` object contains:
- `userClaims` - verified JWT claims from the user's access token.
- `isAdmin` - `true` if the request was made using a valid `x-hasura-admin-secret` header.
- `request` - [Fetch API Request object](https://developer.mozilla.org/en-US/docs/Web/API/Request) that represents the incoming HTTP request in platform-independent way. It can be useful for accessing headers to authenticate a user
- `query` - the DocumentNode that was parsed from the GraphQL query string
- `operationName` - the operation name selected from the incoming query
- `variables` - the variables that were defined in the query
- `extensions` - the extensions that were received from the client
Read more about the [default context from GraphQL Yoga](https://www.the-guild.dev/graphql/yoga-server/docs/features/context#default-context).
[https://docs.nhost.io/graphql/remote-schemas/stripe](https://docs.nhost.io/graphql/remote-schemas/stripe).
## Development

View File

@@ -15,4 +15,6 @@ const server = createStripeGraphQLServer({
graphiql: true
})
server.start()
server.listen(4000, () => {
console.info('Stripe GraphQL API server is running on http://localhost:4000')
})

View File

@@ -1,6 +1,6 @@
{
"name": "@nhost/stripe-graphql-js",
"version": "0.0.8",
"version": "1.0.0",
"description": "Stripe GraphQL API",
"license": "MIT",
"keywords": [
@@ -40,18 +40,18 @@
"verify:fix": "run-p prettier:fix lint:fix"
},
"dependencies": {
"@graphql-yoga/node": "^2.13.13",
"@pothos/core": "^3.21.0",
"graphql": "16.6.0",
"graphql-scalars": "^1.18.0",
"graphql-yoga": "^3.4.0",
"jsonwebtoken": "^9.0.0",
"stripe": "^10.10.0"
"stripe": "^11.8.0"
},
"devDependencies": {
"@types/jsonwebtoken": "^9.0.0",
"@types/node": "^18.11.9",
"dotenv": "^16.0.3",
"ts-node-dev": "^2.0.0",
"typescript": "^4.8.4"
"typescript": "^4.7.4"
}
}

View File

@@ -1,6 +1,6 @@
import Stripe from 'stripe'
import { GraphQLYogaError } from '@graphql-yoga/node'
import { GraphQLError } from 'graphql'
import { builder } from '../builder'
import { stripe } from '../utils'
@@ -12,7 +12,7 @@ builder.objectType('Stripe', {
resolve: async (_parent, _, context) => {
const { isAdmin } = context
if (!isAdmin) throw new GraphQLYogaError('Not allowed')
if (!isAdmin) throw new GraphQLError('Not allowed')
const connectedAccounts = await stripe.accounts.list()
@@ -29,7 +29,7 @@ builder.objectType('Stripe', {
resolve: async (_parent, { id }, context) => {
const { isAdmin } = context
if (!isAdmin) throw new GraphQLYogaError('Not allowed')
if (!isAdmin) throw new GraphQLError('Not allowed')
const connectedAccount = await stripe.accounts.retrieve(id)
@@ -46,14 +46,14 @@ builder.objectType('Stripe', {
resolve: async (_parent, { id }, context) => {
const { isAllowed } = context
if (!await isAllowed(id, context)) {
throw new GraphQLYogaError('Not allowed')
if (!(await isAllowed(id, context))) {
throw new GraphQLError('Not allowed')
}
const customer = await stripe.customers.retrieve(id)
if (customer.deleted) {
throw new GraphQLYogaError('The Stripe customer is deleted')
throw new GraphQLError('The Stripe customer is deleted')
}
return customer
@@ -129,8 +129,8 @@ builder.objectType('StripeMutations', {
resolve: async (_, { customer, configuration, locale, returnUrl }, context) => {
const { isAllowed } = context
if (!await isAllowed(customer, context)) {
throw new GraphQLYogaError('Not allowed')
if (!(await isAllowed(customer, context))) {
throw new GraphQLError('Not allowed')
}
const session = await stripe.billingPortal.sessions.create({

View File

@@ -1,4 +1,5 @@
import { createServer, YogaInitialContext } from '@graphql-yoga/node'
import { createServer } from 'node:http'
import { createYoga, YogaInitialContext } from 'graphql-yoga'
import { schema } from './schema'
import { Context, CreateServerProps } from './types'
@@ -45,12 +46,15 @@ const createStripeGraphQLServer = (params?: CreateServerProps) => {
}
}
return createServer({
const yoga = createYoga({
cors,
graphiql,
context,
schema
schema,
graphqlEndpoint: '*'
})
return createServer(yoga)
}
export { createStripeGraphQLServer, schema }

View File

@@ -1,6 +1,6 @@
import type Stripe from 'stripe'
import type { CORSOptions, YogaInitialContext } from '@graphql-yoga/node'
import type { CORSOptions, YogaInitialContext } from 'graphql-yoga'
export type StripeGraphQLContext = {
isAllowed: (stripeCustomerId: string, context: Context) => boolean | Promise<boolean>

View File

@@ -8,7 +8,7 @@ if (!process.env.STRIPE_SECRET_KEY) {
}
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01'
apiVersion: '2022-11-15'
})
export const getUserClaims = (req: Request): UserHasuraClaims | undefined => {

263
pnpm-lock.yaml generated
View File

@@ -684,7 +684,7 @@ importers:
examples/serverless-functions:
specifiers:
'@graphql-yoga/node': ^2.13.13
'@nhost/stripe-graphql-js': ^0.0.8
'@nhost/stripe-graphql-js': ^1.0.0
'@pothos/core': ^3.21.0
'@types/express': ^4.17.13
cross-fetch: ^3.1.5
@@ -886,30 +886,30 @@ importers:
integrations/stripe-graphql-js:
specifiers:
'@graphql-yoga/node': ^2.13.13
'@pothos/core': ^3.21.0
'@types/jsonwebtoken': ^9.0.0
'@types/node': ^18.11.9
dotenv: ^16.0.3
graphql: 16.6.0
graphql-scalars: ^1.18.0
graphql-yoga: ^3.4.0
jsonwebtoken: ^9.0.0
stripe: ^10.10.0
stripe: ^11.8.0
ts-node-dev: ^2.0.0
typescript: ^4.8.4
typescript: ^4.7.4
dependencies:
'@graphql-yoga/node': 2.13.13_graphql@16.6.0
'@pothos/core': 3.21.0_graphql@16.6.0
graphql: 16.6.0
graphql-scalars: 1.18.0_graphql@16.6.0
graphql-yoga: 3.4.0_xfoe4adolgvm4tvnio5xigcr6e
jsonwebtoken: 9.0.0
stripe: 10.10.0
stripe: 11.8.0
devDependencies:
'@types/jsonwebtoken': 9.0.0
'@types/node': 18.11.9
dotenv: 16.0.3
ts-node-dev: 2.0.0_cbe7ovvae6zqfnmtgctpgpys54
typescript: 4.8.4
ts-node-dev: 2.0.0_vq46kxj6zfka4f6ijsosnft3hy
typescript: 4.7.4
packages/docgen:
specifiers:
@@ -5911,6 +5911,13 @@ packages:
tslib: 2.4.0
dev: false
/@envelop/core/3.0.4:
resolution: {integrity: sha512-AybIZxQsDlFQTWHy6YtX/MSQPVuw+eOFtTW90JsHn6EbmcQnD6N3edQfSiTGjggPRHLoC0+0cuYXp2Ly2r3vrQ==}
dependencies:
'@envelop/types': 3.0.1
tslib: 2.4.0
dev: false
/@envelop/parser-cache/4.7.0_4hr55tbjlvoppd2sokdhrbpreq:
resolution: {integrity: sha512-63NfXDcW/vGn4U6NFxaZ0JbYWAcJb9A6jhTvghsSz1ZS+Dny/ci8bVSgVmM1q+N56hPyGsVPuyI+rIc71mPU5g==}
peerDependencies:
@@ -5923,6 +5930,18 @@ packages:
tslib: 2.4.1
dev: false
/@envelop/parser-cache/5.0.4_a6sekiasy2tqr6d5gj7n2wtjli:
resolution: {integrity: sha512-+kp6nzCVLYI2WQExQcE3FSy6n9ZGB5GYi+ntyjYdxaXU41U1f8RVwiLdyh0Ewn5D/s/zaLin09xkFKITVSAKDw==}
peerDependencies:
'@envelop/core': ^3.0.4
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
dependencies:
'@envelop/core': 3.0.4
graphql: 16.6.0
lru-cache: 6.0.0
tslib: 2.4.1
dev: false
/@envelop/types/2.4.0_graphql@16.6.0:
resolution: {integrity: sha512-pjxS98cDQBS84X29VcwzH3aJ/KiLCGwyMxuj7/5FkdiaCXAD1JEvKEj9LARWlFYj1bY43uII4+UptFebrhiIaw==}
peerDependencies:
@@ -5932,6 +5951,12 @@ packages:
tslib: 2.4.1
dev: false
/@envelop/types/3.0.1:
resolution: {integrity: sha512-Ok62K1K+rlS+wQw77k8Pis8+1/h7+/9Wk5Fgcc2U6M5haEWsLFAHcHsk8rYlnJdEUl2Y3yJcCSOYbt1dyTaU5w==}
dependencies:
tslib: 2.4.1
dev: false
/@envelop/validation-cache/4.7.0_4hr55tbjlvoppd2sokdhrbpreq:
resolution: {integrity: sha512-PzL+GfWJRT+JjsJqZAIxHKEkvkM3hxkeytS5O0QLXT8kURNBV28r+Kdnn2RCF5+6ILhyGpiDb60vaquBi7g4lw==}
peerDependencies:
@@ -5944,6 +5969,18 @@ packages:
tslib: 2.4.1
dev: false
/@envelop/validation-cache/5.0.5_a6sekiasy2tqr6d5gj7n2wtjli:
resolution: {integrity: sha512-69sq5H7hvxE+7VV60i0bgnOiV1PX9GEJHKrBrVvyEZAXqYojKO3DP9jnLGryiPgVaBjN5yw12ge0l0s2gXbolQ==}
peerDependencies:
'@envelop/core': ^3.0.4
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0
dependencies:
'@envelop/core': 3.0.4
graphql: 16.6.0
lru-cache: 6.0.0
tslib: 2.4.1
dev: false
/@esbuild/android-arm/0.16.10:
resolution: {integrity: sha512-RmJjQTRrO6VwUWDrzTBLmV4OJZTarYsiepLGlF2rYTVB701hSorPywPGvP6d8HCuuRibyXa5JX4s3jN2kHEtjQ==}
engines: {node: '>=12'}
@@ -6925,6 +6962,19 @@ packages:
value-or-promise: 1.0.11
dev: true
/@graphql-tools/executor/0.0.12_graphql@16.6.0:
resolution: {integrity: sha512-bWpZcYRo81jDoTVONTnxS9dDHhEkNVjxzvFCH4CRpuyzD3uL+5w3MhtxIh24QyWm4LvQ4f+Bz3eMV2xU2I5+FA==}
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
dependencies:
'@graphql-tools/utils': 9.1.4_graphql@16.6.0
'@graphql-typed-document-node/core': 3.1.1_graphql@16.6.0
'@repeaterjs/repeater': 3.0.4
graphql: 16.6.0
tslib: 2.4.1
value-or-promise: 1.0.12
dev: false
/@graphql-tools/git-loader/7.2.13_graphql@16.6.0:
resolution: {integrity: sha512-PBAzZWXzKUL+VvlUQOjF++246G1O6TTMzvIlxaecgxvTSlnljEXJcDQlxqXhfFPITc5MP7He0N1UcZPBU/DE7Q==}
peerDependencies:
@@ -7364,7 +7414,15 @@ packages:
dependencies:
graphql: 16.6.0
tslib: 2.4.1
dev: true
/@graphql-tools/utils/9.1.4_graphql@16.6.0:
resolution: {integrity: sha512-hgIeLt95h9nQgQuzbbdhuZmh+8WV7RZ/6GbTj6t3IU4Zd2zs9yYJ2jgW/krO587GMOY8zCwrjNOMzD40u3l7Vg==}
peerDependencies:
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
dependencies:
graphql: 16.6.0
tslib: 2.4.1
dev: false
/@graphql-tools/wrap/8.5.1_graphql@16.6.0:
resolution: {integrity: sha512-KpVVfha2wLSpE08YLX0jeo5nXPfDLASlxOqMlvfa/B4X8SOVmuLyN1L5YZ132tPLDF93uflwlHFnUO5ahpRNlA==}
@@ -7430,6 +7488,15 @@ packages:
tslib: 2.4.1
dev: false
/@graphql-yoga/subscription/3.1.0:
resolution: {integrity: sha512-Vc9lh8KzIHyS3n4jBlCbz7zCjcbtQnOBpsymcRvHhFr2cuH+knmRn0EmzimMQ58jQ8kxoRXXC3KJS3RIxSdPIg==}
dependencies:
'@graphql-yoga/typed-event-target': 1.0.0
'@repeaterjs/repeater': 3.0.4
'@whatwg-node/events': 0.0.2
tslib: 2.4.1
dev: false
/@graphql-yoga/typed-event-target/0.1.1:
resolution: {integrity: sha512-l23kLKHKhfD7jmv4OUlzxMTihSqgIjGWCSb0KdlLkeiaF2jjuo8pRhX200hFTrtjRHGSYS1fx2lltK/xWci+vw==}
dependencies:
@@ -7437,6 +7504,13 @@ packages:
tslib: 2.4.1
dev: false
/@graphql-yoga/typed-event-target/1.0.0:
resolution: {integrity: sha512-Mqni6AEvl3VbpMtKw+TIjc9qS9a8hKhiAjFtqX488yq5oJtj9TkNlFTIacAVS3vnPiswNsmDiQqvwUOcJgi1DA==}
dependencies:
'@repeaterjs/repeater': 3.0.4
tslib: 2.4.1
dev: false
/@grpc/grpc-js/1.7.3:
resolution: {integrity: sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==}
engines: {node: ^8.13.0 || >=10.10.0}
@@ -11699,7 +11773,6 @@ packages:
/@types/node/18.11.9:
resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==}
dev: true
/@types/normalize-package-data/2.4.1:
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
@@ -13223,6 +13296,10 @@ packages:
'@xtuc/long': 4.2.2
dev: true
/@whatwg-node/events/0.0.2:
resolution: {integrity: sha512-WKj/lI4QjnLuPrim0cfO7i+HsDSXHxNv1y0CrJhdntuO3hxWZmnXCwNDnwOvry11OjRin6cgWNF+j/9Pn8TN4w==}
dev: false
/@whatwg-node/fetch/0.2.6:
resolution: {integrity: sha512-NhHiqeGcKjgqUZvJTZSou9qsFEPBBG1LPm2Npz0cmcPvukhhQfjX+p3quRx6b9AyjNPp1f73VB1z4ApHy9FcNg==}
dependencies:
@@ -13269,6 +13346,34 @@ packages:
- encoding
dev: true
/@whatwg-node/fetch/0.6.2:
resolution: {integrity: sha512-fCUycF1W+bI6XzwJFnbdDuxIldfKM3w8+AzVCLGlucm0D+AQ8ZMm2j84hdcIhfV6ZdE4Y1HFVrHosAxdDZ+nPw==}
dependencies:
'@peculiar/webcrypto': 1.4.0
abort-controller: 3.0.0
busboy: 1.6.0
form-data-encoder: 1.7.2
formdata-node: 4.3.2
node-fetch: 2.6.7
undici: 5.12.0
urlpattern-polyfill: 6.0.2
web-streams-polyfill: 3.2.1
transitivePeerDependencies:
- encoding
dev: false
/@whatwg-node/server/0.5.8_@types+node@18.11.9:
resolution: {integrity: sha512-29f2Ijk663Hr6hF5GU5a8ELGQVbNMMDBWF1lTdpIKGyLrLJTKixarp6COEyEN5H9tGzIRUQar9Z76A+Jb9DyzQ==}
peerDependencies:
'@types/node': ^18.0.6
dependencies:
'@types/node': 18.11.9
'@whatwg-node/fetch': 0.6.2
tslib: 2.4.1
transitivePeerDependencies:
- encoding
dev: false
/@wry/context/0.6.1:
resolution: {integrity: sha512-LOmVnY1iTU2D8tv4Xf6MVMZZ+juIJ87Kt/plMijjN20NMAXGmH4u8bS1t0uT74cZ5gwpocYueV58YwyI8y+GKw==}
engines: {node: '>=8'}
@@ -14092,7 +14197,7 @@ packages:
/axios/0.25.0_debug@4.3.4:
resolution: {integrity: sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==}
dependencies:
follow-redirects: 1.15.2
follow-redirects: 1.15.2_debug@4.3.4
transitivePeerDependencies:
- debug
dev: true
@@ -19529,6 +19634,19 @@ packages:
peerDependenciesMeta:
debug:
optional: true
dev: false
/follow-redirects/1.15.2_debug@4.3.4:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dependencies:
debug: 4.3.4
dev: true
/for-each/0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@@ -20493,6 +20611,28 @@ packages:
dependencies:
graphql: 16.6.0
/graphql-yoga/3.4.0_xfoe4adolgvm4tvnio5xigcr6e:
resolution: {integrity: sha512-Cjx60mmpoK1qL/sLdM285VdAOQyJBKLuC6oMZrfO8QleneNtu0nDOM6Efv5m0IrRYSONEMtIYA7eNr0u/cCBfg==}
peerDependencies:
graphql: ^15.2.0 || ^16.0.0
dependencies:
'@envelop/core': 3.0.4
'@envelop/parser-cache': 5.0.4_a6sekiasy2tqr6d5gj7n2wtjli
'@envelop/validation-cache': 5.0.5_a6sekiasy2tqr6d5gj7n2wtjli
'@graphql-tools/executor': 0.0.12_graphql@16.6.0
'@graphql-tools/schema': 9.0.4_graphql@16.6.0
'@graphql-tools/utils': 9.1.1_graphql@16.6.0
'@graphql-yoga/subscription': 3.1.0
'@whatwg-node/fetch': 0.6.2
'@whatwg-node/server': 0.5.8_@types+node@18.11.9
dset: 3.1.2
graphql: 16.6.0
tslib: 2.4.1
transitivePeerDependencies:
- '@types/node'
- encoding
dev: false
/graphql/16.6.0:
resolution: {integrity: sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
@@ -28412,14 +28552,6 @@ packages:
acorn: 8.8.1
dev: true
/stripe/10.10.0:
resolution: {integrity: sha512-CINNkuDEmF1rsHoTKS39iZ641NFONKZKllUioeQkRpz7qYPJayp7Xl/V30vbM2n5CTJo3X0FmZ2CychThfPTDA==}
engines: {node: ^8.1 || >=10.*}
dependencies:
'@types/node': 18.11.17
qs: 6.11.0
dev: false
/stripe/10.17.0:
resolution: {integrity: sha512-JHV2KoL+nMQRXu3m9ervCZZvi4DDCJfzHUE6CmtJxR9TmizyYfrVuhGvnsZLLnheby9Qrnf4Hq6iOEcejGwnGQ==}
engines: {node: ^8.1 || >=10.*}
@@ -28436,6 +28568,14 @@ packages:
qs: 6.11.0
dev: false
/stripe/11.8.0:
resolution: {integrity: sha512-aGwrJDqYzpjQj0ejt7oN7BE7kUjZFxhUz/gDeyDCS7CBpZhDb26Eb6z9sS8KdbsbmuS8rkkn2lBY4koK7L1ZCw==}
engines: {node: '>=12.*'}
dependencies:
'@types/node': 18.11.17
qs: 6.11.0
dev: false
/stubs/3.0.0:
resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==}
dev: false
@@ -29224,7 +29364,7 @@ packages:
code-block-writer: 11.0.3
dev: true
/ts-node-dev/2.0.0_cbe7ovvae6zqfnmtgctpgpys54:
/ts-node-dev/2.0.0_vq46kxj6zfka4f6ijsosnft3hy:
resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==}
engines: {node: '>=0.8.0'}
hasBin: true
@@ -29243,9 +29383,9 @@ packages:
rimraf: 2.7.1
source-map-support: 0.5.21
tree-kill: 1.2.2
ts-node: 10.9.1_cbe7ovvae6zqfnmtgctpgpys54
ts-node: 10.9.1_vq46kxj6zfka4f6ijsosnft3hy
tsconfig: 7.0.0
typescript: 4.8.4
typescript: 4.7.4
transitivePeerDependencies:
- '@swc/core'
- '@swc/wasm'
@@ -29283,37 +29423,6 @@ packages:
yn: 3.1.1
dev: true
/ts-node/10.9.1_cbe7ovvae6zqfnmtgctpgpys54:
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
hasBin: true
peerDependencies:
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
'@types/node': '*'
typescript: '>=2.7'
peerDependenciesMeta:
'@swc/core':
optional: true
'@swc/wasm':
optional: true
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.8
'@tsconfig/node12': 1.0.9
'@tsconfig/node14': 1.0.1
'@tsconfig/node16': 1.0.2
'@types/node': 18.11.9
acorn: 8.7.1
acorn-walk: 8.2.0
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 4.8.4
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
dev: true
/ts-node/10.9.1_kdoazjjbodcwagz6yx6a7ztgpu:
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
hasBin: true
@@ -29437,6 +29546,37 @@ packages:
yn: 3.1.1
dev: true
/ts-node/10.9.1_vq46kxj6zfka4f6ijsosnft3hy:
resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==}
hasBin: true
peerDependencies:
'@swc/core': '>=1.2.50'
'@swc/wasm': '>=1.2.50'
'@types/node': '*'
typescript: '>=2.7'
peerDependenciesMeta:
'@swc/core':
optional: true
'@swc/wasm':
optional: true
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.8
'@tsconfig/node12': 1.0.9
'@tsconfig/node14': 1.0.1
'@tsconfig/node16': 1.0.2
'@types/node': 18.11.9
acorn: 8.7.1
acorn-walk: 8.2.0
arg: 4.1.3
create-require: 1.1.1
diff: 4.0.2
make-error: 1.3.6
typescript: 4.7.4
v8-compile-cache-lib: 3.0.1
yn: 3.1.1
dev: true
/ts-pnp/1.2.0_typescript@4.9.3:
resolution: {integrity: sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==}
engines: {node: '>=6'}
@@ -29732,6 +29872,12 @@ packages:
hasBin: true
dev: true
/typescript/4.7.4:
resolution: {integrity: sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==}
engines: {node: '>=4.2.0'}
hasBin: true
dev: true
/typescript/4.8.3:
resolution: {integrity: sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==}
engines: {node: '>=4.2.0'}
@@ -30217,6 +30363,12 @@ packages:
querystring: 0.2.0
dev: true
/urlpattern-polyfill/6.0.2:
resolution: {integrity: sha512-5vZjFlH9ofROmuWmXM9yj2wljYKgWstGwe8YTyiqM7hVum/g9LyCizPZtb3UqsuppVwety9QJmfc42VggLpTgg==}
dependencies:
braces: 3.0.2
dev: false
/urql/3.0.3_onqnqwb3ubg5opvemcqf7c2qhy:
resolution: {integrity: sha512-aVUAMRLdc5AOk239DxgXt6ZxTl/fEmjr7oyU5OGo8uvpqu42FkeJErzd2qBzhAQ3DyusoZIbqbBLPlnKo/yy2A==}
peerDependencies:
@@ -30474,6 +30626,11 @@ packages:
resolution: {integrity: sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==}
engines: {node: '>=12'}
/value-or-promise/1.0.12:
resolution: {integrity: sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==}
engines: {node: '>=12'}
dev: false
/vary/1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}