Compare commits
163 Commits
@nhost/das
...
@nhost/nho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c6f1e3b33 | ||
|
|
d1365ea516 | ||
|
|
72dbba7881 | ||
|
|
a3f3991d5a | ||
|
|
c71fe2cf72 | ||
|
|
24c5ed3ea4 | ||
|
|
2d9145f918 | ||
|
|
9a0ab5b887 | ||
|
|
1ddf704c5b | ||
|
|
6f4ee845c6 | ||
|
|
76ce7d7b6e | ||
|
|
538bfbcb3e | ||
|
|
07b35d1e5f | ||
|
|
2200a0ed07 | ||
|
|
df23d97126 | ||
|
|
104f149369 | ||
|
|
01228583a0 | ||
|
|
93309dd851 | ||
|
|
2cc18dcb51 | ||
|
|
3b48a62790 | ||
|
|
8897dec056 | ||
|
|
324dda8309 | ||
|
|
95f62bed07 | ||
|
|
0e4d8ff118 | ||
|
|
baec5bada7 | ||
|
|
4e56cfc628 | ||
|
|
54bc91923f | ||
|
|
77b12feb95 | ||
|
|
32d4670bbb | ||
|
|
1dc09942d2 | ||
|
|
3343a36358 | ||
|
|
b755e9086c | ||
|
|
48866d0ee1 | ||
|
|
5b3b76bd41 | ||
|
|
284ef7e7f2 | ||
|
|
6d5c202da9 | ||
|
|
9342937440 | ||
|
|
e89cd4e262 | ||
|
|
a05438352b | ||
|
|
78437959bb | ||
|
|
e1a7433adb | ||
|
|
e23cf74975 | ||
|
|
a3d01c4fad | ||
|
|
4cdcef9ef5 | ||
|
|
df894ef7e2 | ||
|
|
f7dd6a9fc6 | ||
|
|
2949ff0f62 | ||
|
|
1527b0a455 | ||
|
|
375e53a3f0 | ||
|
|
96e3ca5a32 | ||
|
|
0e570df9c5 | ||
|
|
1f4c67283e | ||
|
|
fc1c4861a3 | ||
|
|
74feaf6add | ||
|
|
8cd97206cc | ||
|
|
02197639f2 | ||
|
|
38b594aef9 | ||
|
|
f3a8886cd0 | ||
|
|
8d76cf8d40 | ||
|
|
3e1fb974e4 | ||
|
|
f74871d872 | ||
|
|
3f26056688 | ||
|
|
6a7801be93 | ||
|
|
7bc5bb857c | ||
|
|
c957039d75 | ||
|
|
96c4032424 | ||
|
|
ec70126b56 | ||
|
|
86b9f9040c | ||
|
|
222f03725b | ||
|
|
10b786e5c6 | ||
|
|
aa8ae88d12 | ||
|
|
0f2c86b41a | ||
|
|
a4c76892dd | ||
|
|
00d278b2cc | ||
|
|
cb6b5faeb9 | ||
|
|
7c4c847b91 | ||
|
|
908887d8c5 | ||
|
|
a2d67bc2db | ||
|
|
1a6cd78254 | ||
|
|
6500629c4b | ||
|
|
add3c2c10e | ||
|
|
dd29b06260 | ||
|
|
490cb25a0f | ||
|
|
0df0dd741e | ||
|
|
2172946879 | ||
|
|
40e50f0e75 | ||
|
|
65cf0888b5 | ||
|
|
21833019ca | ||
|
|
b3171ba3e9 | ||
|
|
6f01f19d02 | ||
|
|
ce92b01eac | ||
|
|
e24a177434 | ||
|
|
56a52b6d48 | ||
|
|
92bfa8c723 | ||
|
|
2a52aaa4a6 | ||
|
|
8280a3e9d8 | ||
|
|
523f60bf68 | ||
|
|
19b11d4084 | ||
|
|
805bae1507 | ||
|
|
f6c014c06f | ||
|
|
c5794f4596 | ||
|
|
fc28817380 | ||
|
|
80bbd3a165 | ||
|
|
7a10617a72 | ||
|
|
f0b6dca1a5 | ||
|
|
5db20adc34 | ||
|
|
12dc41a517 | ||
|
|
768fd56891 | ||
|
|
8a508cb1cc | ||
|
|
34f6a8eef4 | ||
|
|
c9d2d31a9b | ||
|
|
68fb23a361 | ||
|
|
476139e528 | ||
|
|
6a850818a0 | ||
|
|
3970dbba0d | ||
|
|
8ee2166f0d | ||
|
|
e13500a185 | ||
|
|
411f574a51 | ||
|
|
7fc91b992e | ||
|
|
b840012be0 | ||
|
|
645c51a9dc | ||
|
|
0ce6f05539 | ||
|
|
8b1188af53 | ||
|
|
12b01f8dee | ||
|
|
60f4faf409 | ||
|
|
528dff3f1b | ||
|
|
d429fb4a3e | ||
|
|
816c916709 | ||
|
|
b7a2b8b537 | ||
|
|
261d8cf434 | ||
|
|
41f49bde76 | ||
|
|
65f685bdb2 | ||
|
|
f52a7f4aac | ||
|
|
e71b9903d9 | ||
|
|
325fd08aef | ||
|
|
3888704464 | ||
|
|
38e8a10a29 | ||
|
|
d8545eae12 | ||
|
|
3d5bfd87d2 | ||
|
|
e66c5626bd | ||
|
|
a227c6561e | ||
|
|
e885c159df | ||
|
|
09fcb74bef | ||
|
|
a089197197 | ||
|
|
34f843875b | ||
|
|
ca278a8c39 | ||
|
|
75603786e0 | ||
|
|
4e4e699b94 | ||
|
|
da31fa9fba | ||
|
|
95e2afaf47 | ||
|
|
958a56dde9 | ||
|
|
74cb15930e | ||
|
|
aa37a98424 | ||
|
|
11cbdda3a5 | ||
|
|
6d1f4adf10 | ||
|
|
ddbc50c15e | ||
|
|
b2cbf570a3 | ||
|
|
22b8e65031 | ||
|
|
63c94d2036 | ||
|
|
010df48c1e | ||
|
|
fdc11db93d | ||
|
|
cb4749f168 | ||
|
|
46a8fcf471 |
@@ -40,14 +40,14 @@ runs:
|
||||
- shell: bash
|
||||
name: Build packages
|
||||
if: ${{ inputs.BUILD == 'all' }}
|
||||
run: pnpm build:all
|
||||
run: pnpm run build:all
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
- shell: bash
|
||||
name: Build everything in the monorepo
|
||||
if: ${{ inputs.BUILD == 'default' }}
|
||||
run: pnpm build
|
||||
run: pnpm run build
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
|
||||
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@@ -8,7 +8,6 @@ on:
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths-ignore:
|
||||
- 'assets/**'
|
||||
@@ -56,7 +55,7 @@ jobs:
|
||||
| xargs -I@ realpath --relative-to=$PWD @ \
|
||||
| xargs -I@ jq "if (.scripts.e2e | length) != 0 then {name: .name, path: \"@\"} else null end" @/package.json \
|
||||
| awk "!/null/" \
|
||||
| jq -c --slurp)
|
||||
| jq -c --slurp 'map(select(length > 0))')
|
||||
echo "matrix=$PACKAGES" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
"@nhost/docgen": [
|
||||
"../packages/docgen/src/index.ts"
|
||||
],
|
||||
"@nhost/graphql-js": [
|
||||
"../packages/graphql-js/src/index.ts"
|
||||
],
|
||||
"@nhost/hasura-auth-js": [
|
||||
"../packages/hasura-auth-js/src/index.ts"
|
||||
],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import replace from '@rollup/plugin-replace'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import dts from 'vite-plugin-dts'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
@@ -61,7 +60,6 @@ export default defineConfig({
|
||||
'@apollo/client/utilities': '@apollo/client/utilities',
|
||||
'graphql-ws': 'graphql-ws',
|
||||
xstate: 'xstate',
|
||||
axios: 'axios',
|
||||
'js-cookie': 'Cookies',
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
# @nhost/dashboard
|
||||
|
||||
## 0.11.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- b755e908: fix(dashboard): use correct date for last seen
|
||||
- 2d9145f9: chore(deps): revert GraphQL client
|
||||
- 1ddf704c: fix(dashboard): don't show false positive message for failed user creation
|
||||
- @nhost/react-apollo@5.0.3
|
||||
- @nhost/nextjs@1.13.8
|
||||
|
||||
## 0.11.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react-apollo@5.0.2
|
||||
- @nhost/nextjs@1.13.7
|
||||
|
||||
## 0.11.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2cc18dcb: fix(dashboard): prevent permission editor dropdown from being always open
|
||||
|
||||
## 0.11.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3343a363: chore(dashboard): bump `@testing-library/react` to v14 and `@testing-library/dom` to v9
|
||||
- @nhost/react-apollo@5.0.1
|
||||
- @nhost/nextjs@1.13.6
|
||||
|
||||
## 0.11.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.11.12",
|
||||
"version": "0.11.16",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -44,7 +44,6 @@
|
||||
"@tanstack/react-table": "^8.5.30",
|
||||
"@tanstack/react-virtual": "^3.0.0-beta.23",
|
||||
"analytics-node": "^6.2.0",
|
||||
"axios": "^0.27.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"clsx": "^1.2.1",
|
||||
"cross-fetch": "^3.1.5",
|
||||
@@ -98,9 +97,9 @@
|
||||
"@storybook/manager-webpack5": "^6.5.14",
|
||||
"@storybook/react": "^6.5.14",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@testing-library/dom": "^8.19.0",
|
||||
"@testing-library/dom": "^9.0.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^16.11.7",
|
||||
|
||||
@@ -213,7 +213,6 @@ export default function RuleValueInput({
|
||||
freeSolo={!isHasuraInput}
|
||||
autoSelect={!isHasuraInput}
|
||||
autoHighlight={isHasuraInput}
|
||||
open
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
if (typeof value === 'string') {
|
||||
return option.value.toLowerCase() === (value as string).toLowerCase();
|
||||
|
||||
@@ -277,7 +277,7 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
|
||||
}
|
||||
|
||||
if (fileError) {
|
||||
throw fileError;
|
||||
throw new Error(fileError.message);
|
||||
}
|
||||
|
||||
triggerToast(`File has been uploaded successfully (${fileMetadata?.id})`);
|
||||
|
||||
@@ -67,7 +67,7 @@ export function InviteAnnounce() {
|
||||
triggerToast('An error occurred when trying to accept the invitation.');
|
||||
|
||||
return setSubmitState({
|
||||
error: res.error,
|
||||
error: new Error(res.error.message),
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
),
|
||||
},
|
||||
{ key: 'NHOST_AUTH_URL', value: appClient.auth.url },
|
||||
{ key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.url },
|
||||
{ key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.httpUrl },
|
||||
{ key: 'NHOST_STORAGE_URL', value: appClient.storage.url },
|
||||
{ key: 'NHOST_FUNCTIONS_URL', value: appClient.functions.url },
|
||||
];
|
||||
|
||||
@@ -6,7 +6,7 @@ import Input from '@/ui/v2/Input';
|
||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import axios from 'axios';
|
||||
import fetch from 'cross-fetch';
|
||||
import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
@@ -79,28 +79,36 @@ export default function CreateUserForm({
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
axios.post(signUpUrl, {
|
||||
email,
|
||||
password,
|
||||
fetch(signUpUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, password }),
|
||||
}).then(async (res) => {
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (res.status === 409) {
|
||||
setError('email', { message: data?.message });
|
||||
}
|
||||
|
||||
throw new Error(data?.message || 'Something went wrong.');
|
||||
}),
|
||||
{
|
||||
loading: 'Creating user...',
|
||||
success: 'User created successfully.',
|
||||
error: 'An error occurred while trying to create the user.',
|
||||
error: (arg) =>
|
||||
arg?.message
|
||||
? `Error: ${arg.message}`
|
||||
: 'An error occurred while trying to create the user.',
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
if (error.response?.status === 409) {
|
||||
setError('email', {
|
||||
message: error.response.data.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setCreateUserFormError(
|
||||
new Error(error.response.data.message || 'Something went wrong.'),
|
||||
);
|
||||
} catch {
|
||||
// Note: Error is already handled by toast.promise
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +145,7 @@ export default function CreateUserForm({
|
||||
{createUserFormError && (
|
||||
<Alert
|
||||
severity="error"
|
||||
className="grid items-center justify-between grid-flow-col px-4 py-3"
|
||||
className="grid grid-flow-col items-center justify-between px-4 py-3"
|
||||
>
|
||||
<span className="text-left">
|
||||
<strong>Error:</strong> {createUserFormError.message}
|
||||
|
||||
@@ -268,7 +268,7 @@ export default function EditUserForm({
|
||||
Created At
|
||||
</InputLabel>
|
||||
<Text className="col-span-3 font-medium">
|
||||
{format(new Date(user.createdAt), 'yyyy-MM-dd hh:mm:ss')}
|
||||
{format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm:ss')}
|
||||
</Text>
|
||||
|
||||
<InputLabel as="h3" className="col-span-1 self-center ">
|
||||
@@ -276,7 +276,7 @@ export default function EditUserForm({
|
||||
</InputLabel>
|
||||
<Text className="col-span-3 font-medium">
|
||||
{user.lastSeen
|
||||
? `${format(new Date(user.lastSeen), 'yyyy-mm-dd hh:mm:ss')}`
|
||||
? `${format(new Date(user.lastSeen), 'yyyy-MM-dd HH:mm:ss')}`
|
||||
: '-'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
@@ -28,7 +28,7 @@ The GraphQL API is available at `https://[subdomain].graphql.[region].nhost.run/
|
||||
|
||||
## GraphQL Clients for JavaScript
|
||||
|
||||
The [Nhost JavaScript client](/reference/javascript) comes with a simple [GraphQL client](/reference/javascript/nhost-js/graphql) that works well for the backend or simple applications.
|
||||
The [Nhost JavaScript client](/reference/javascript) comes with a simple [GraphQL client](/reference/javascript/graphql) that works well for the backend or simple applications.
|
||||
|
||||
When building more complex frontend applications, we recommend using a more advanced GraphQL client such as:
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ In this section:
|
||||
- [Overview](/reference/javascript)
|
||||
- [Authentication](/reference/javascript/auth)
|
||||
- [Storage](/reference/javascript/storage)
|
||||
- [Functions](/reference/javascript/nhost-js/functions)
|
||||
- [GraphQL](/reference/javascript/nhost-js/graphql)
|
||||
- [Functions](/reference/javascript/functions)
|
||||
- [GraphQL](/reference/javascript/graphql)
|
||||
|
||||
### React
|
||||
|
||||
|
||||
60
docs/docs/reference/javascript/functions/content/01-call.mdx
Normal file
60
docs/docs/reference/javascript/functions/content/01-call.mdx
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: call()
|
||||
sidebar_label: call()
|
||||
slug: /reference/javascript/functions/call
|
||||
description: Use `nhost.functions.call` to call (sending a POST request to) a serverless function.
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/index.ts#L55
|
||||
---
|
||||
|
||||
# `call()`
|
||||
|
||||
## Overload 1 of 2
|
||||
|
||||
Use `nhost.functions.call` to call (sending a POST request to) a serverless function.
|
||||
|
||||
:::caution Deprecated
|
||||
Axios will be replaced by cross-fetch in the near future. Only the headers configuration will be kept.
|
||||
:::
|
||||
|
||||
### Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">url</span>** <span className="optional-status">required</span> <code>string</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">data</span>** <span className="optional-status">optional</span> <code>D</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">config</span>** <span className="optional-status">optional</span> <code>AxiosRequestConfig<any> & { useAxios: "true" } & [`NhostFunctionCallConfig`](/reference/javascript/functions/types/nhost-function-call-config) & { useAxios: "true" }</code>
|
||||
|
||||
---
|
||||
|
||||
## Overload 2 of 2
|
||||
|
||||
Use `nhost.functions.call` to call (sending a POST request to) a serverless function.
|
||||
|
||||
```ts
|
||||
await nhost.functions.call('send-welcome-email', {
|
||||
email: 'joe@example.com',
|
||||
name: 'Joe Doe'
|
||||
})
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">url</span>** <span className="optional-status">required</span> <code>string</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">data</span>** <span className="optional-status">required</span> <code>D</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">config</span>** <span className="optional-status">optional</span> <code>[`NhostFunctionCallConfig`](/reference/javascript/functions/types/nhost-function-call-config) & { useAxios: "false" }</code>
|
||||
|
||||
---
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: setAccessToken()
|
||||
sidebar_label: setAccessToken()
|
||||
slug: /reference/javascript/functions/set-access-token
|
||||
description: Use `nhost.functions.setAccessToken` to a set an access token to be used in subsequent functions requests. Note that if you're signin in users with `nhost.auth.signIn()` the access token will be set automatically.
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/index.ts#L155
|
||||
---
|
||||
|
||||
# `setAccessToken()`
|
||||
|
||||
Use `nhost.functions.setAccessToken` to a set an access token to be used in subsequent functions requests. Note that if you're signin in users with `nhost.auth.signIn()` the access token will be set automatically.
|
||||
|
||||
```ts
|
||||
nhost.functions.setAccessToken('some-access-token')
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">accessToken</span>** <span className="optional-status">required</span> <code>undefined | string</code>
|
||||
|
||||
---
|
||||
22
docs/docs/reference/javascript/functions/index.mdx
Normal file
22
docs/docs/reference/javascript/functions/index.mdx
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: NhostFunctionsClient
|
||||
sidebar_label: Functions
|
||||
description: No description provided.
|
||||
slug: /reference/javascript/functions
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/docs/docs/reference/javascript/functions/index.mdx
|
||||
---
|
||||
|
||||
# `NhostFunctionsClient`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">params</span>** <span className="optional-status">required</span> [`NhostFunctionsConstructorParams`](/reference/javascript/functions/types/nhost-functions-constructor-params)
|
||||
|
||||
| Property | Type | Required | Notes |
|
||||
| :--------------------------------------------------------------------------------------------- | :------------------ | :------: | :---------------------------------------------------------------------------------------- |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>url</span> | <code>string</code> | ✔️ | Serverless Functions endpoint. |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>adminSecret</span> | <code>string</code> | | Admin secret. When set, it is sent as an `x-hasura-admin-secret` header for all requests. |
|
||||
|
||||
---
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: NhostFunctionCallConfig
|
||||
sidebar_label: NhostFunctionCallConfig
|
||||
description: Subset of RequestInit parameters that are supported by the functions client
|
||||
displayed_sidebar: referenceSidebar
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/types.ts#L41
|
||||
---
|
||||
|
||||
# `NhostFunctionCallConfig`
|
||||
|
||||
Subset of RequestInit parameters that are supported by the functions client
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">headers</span>** <span className="optional-status">optional</span> <code>Record<string, string></code>
|
||||
|
||||
---
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: NhostFunctionCallResponse
|
||||
sidebar_label: NhostFunctionCallResponse
|
||||
description: No description provided.
|
||||
displayed_sidebar: referenceSidebar
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/types.ts#L15
|
||||
---
|
||||
|
||||
# `NhostFunctionCallResponse`
|
||||
|
||||
```ts
|
||||
type NhostFunctionCallResponse =
|
||||
| { res: { data: T; status: number; statusText: string }; error: null }
|
||||
| { res: null; error: ErrorPayload }
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: NhostFunctionsConstructorParams
|
||||
sidebar_label: NhostFunctionsConstructorParams
|
||||
description: No description provided.
|
||||
displayed_sidebar: referenceSidebar
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/types.ts#L4
|
||||
---
|
||||
|
||||
# `NhostFunctionsConstructorParams`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">url</span>** <span className="optional-status">required</span> <code>string</code>
|
||||
|
||||
Serverless Functions endpoint.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">adminSecret</span>** <span className="optional-status">optional</span> <code>string</code>
|
||||
|
||||
Admin secret. When set, it is sent as an `x-hasura-admin-secret` header for all requests.
|
||||
|
||||
---
|
||||
@@ -10,12 +10,12 @@ The Nhost JavaScript client is the primary way of interacting with your Nhost pr
|
||||
|
||||
- [Authentication](/reference/javascript/auth)
|
||||
- [Storage](/reference/javascript/storage)
|
||||
- [Functions](/reference/javascript/nhost-js/functions)
|
||||
- [GraphQL](/reference/javascript/nhost-js/graphql)
|
||||
- [Functions](/reference/javascript/functions)
|
||||
- [GraphQL](/reference/javascript/graphql)
|
||||
|
||||
## Installation
|
||||
|
||||
Install the the Nhost client together with GraphQL:
|
||||
Install the Nhost client together with GraphQL:
|
||||
|
||||
<Tabs groupId="package-manager">
|
||||
<TabItem value="npm" label="npm" default>
|
||||
|
||||
@@ -111,12 +111,12 @@ const sidebars = {
|
||||
label: 'Functions',
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'reference/docgen/javascript/nhost-js/content/nhost-functions-client/index'
|
||||
id: 'reference/javascript/functions/index'
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: 'reference/docgen/javascript/nhost-js/content/nhost-functions-client/content'
|
||||
dirName: 'reference/javascript/functions/content'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -125,12 +125,12 @@ const sidebars = {
|
||||
label: 'GraphQL',
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'reference/docgen/javascript/nhost-js/content/nhost-graphql-client/index'
|
||||
id: 'reference/docgen/javascript/graphql/content/nhost-graphql-client/index'
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: 'reference/docgen/javascript/nhost-js/content/nhost-graphql-client/content'
|
||||
dirName: 'reference/docgen/javascript/graphql/content/nhost-graphql-client/content'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ export function authProtected(Comp) {
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
return <Comp {...props} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
"@types/react": "18.0.25",
|
||||
"@xstate/inspect": "^0.6.2",
|
||||
"eslint-config-next": "12.0.10",
|
||||
"typescript": "4.5.5",
|
||||
"typescript": "^4.8.2",
|
||||
"ws": "^8.8.1",
|
||||
"xstate": "^4.33.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"version": "0.0.5",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prettier": "prettier --check src/",
|
||||
"prettier:fix": "prettier --write src/",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@xstate/inspect": "^0.6.2",
|
||||
"sass": "1.32.0",
|
||||
"typescript": "^4.8.4",
|
||||
"typescript": "4.9.4",
|
||||
"vite": "^4.0.2",
|
||||
"vue-tsc": "^0.38.9"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @nhost/apollo
|
||||
|
||||
## 5.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [2d9145f9]
|
||||
- @nhost/nhost-js@2.0.2
|
||||
|
||||
## 5.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/nhost-js@2.0.1
|
||||
|
||||
## 5.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c9d2d31a]
|
||||
- Updated dependencies [80bbd3a1]
|
||||
- Updated dependencies [80bbd3a1]
|
||||
- Updated dependencies [2949ff0f]
|
||||
- @nhost/nhost-js@2.0.0
|
||||
|
||||
## 4.13.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/apollo",
|
||||
"version": "4.13.4",
|
||||
"version": "5.0.2",
|
||||
"description": "Nhost Apollo Client library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# @nhost/react-apollo
|
||||
|
||||
## 5.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/apollo@5.0.2
|
||||
- @nhost/react@2.0.2
|
||||
|
||||
## 5.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/apollo@5.0.1
|
||||
- @nhost/react@2.0.1
|
||||
|
||||
## 5.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [19b11d40]
|
||||
- Updated dependencies [19b11d40]
|
||||
- @nhost/react@2.0.0
|
||||
- @nhost/apollo@5.0.0
|
||||
|
||||
## 4.13.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react-apollo",
|
||||
"version": "4.13.5",
|
||||
"version": "5.0.3",
|
||||
"description": "Nhost React Apollo client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# @nhost/react-urql
|
||||
|
||||
## 2.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@2.0.2
|
||||
|
||||
## 2.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- @nhost/react@2.0.1
|
||||
|
||||
## 2.0.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [19b11d40]
|
||||
- Updated dependencies [19b11d40]
|
||||
- @nhost/react@2.0.0
|
||||
|
||||
## 1.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/react-urql",
|
||||
"version": "1.0.5",
|
||||
"version": "2.0.2",
|
||||
"description": "Nhost React URQL client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
9
packages/graphql-js/.eslintrc.js
Normal file
9
packages/graphql-js/.eslintrc.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const base = require('../../config/.eslintrc.js')
|
||||
module.exports = {
|
||||
...base,
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname
|
||||
},
|
||||
ignorePatterns: [...base.ignorePatterns, 'functions/**/*.ts']
|
||||
}
|
||||
14
packages/graphql-js/CHANGELOG.md
Normal file
14
packages/graphql-js/CHANGELOG.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# @nhost/graphql-js
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2d9145f9: chore(deps): revert GraphQL client
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 2200a0ed: Correct type inference on snake case operations
|
||||
- 3b48a627: Improve readme instructions
|
||||
16
packages/graphql-js/README.md
Normal file
16
packages/graphql-js/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
<h1 align="center">@nhost/graphql-js</h1>
|
||||
<h2 align="center">Nhost GraphQL client</h2>
|
||||
|
||||
<p align="center">
|
||||
<img alt="npm" src="https://img.shields.io/npm/v/@nhost/graphql-js">
|
||||
<img alt="npm" src="https://img.shields.io/npm/dm/@nhost/graphql-js">
|
||||
<a href="LICENSE">
|
||||
<img src="https://img.shields.io/badge/license-MIT-yellow.svg" alt="license: MIT" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Nhost GraphQL client.
|
||||
|
||||
## Documentation
|
||||
|
||||
[https://docs.nhost.io/reference/javascript/graphql](https://docs.nhost.io/reference/javascript/graphql)
|
||||
10
packages/graphql-js/graphql.docgen.json
Normal file
10
packages/graphql-js/graphql.docgen.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"title": "GraphQL",
|
||||
"path": "./.docgen/graphql.json",
|
||||
"output": "../../docs/docs/reference/docgen/javascript/graphql",
|
||||
"root": "reference/docgen/javascript/graphql",
|
||||
"slug": "/reference/javascript/graphql",
|
||||
"sidebarConfig": "referenceSidebar",
|
||||
"baseEditUrl": "https://github.com/nhost/nhost/edit/main/packages",
|
||||
"cleanup": true
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
"sort": [
|
||||
"source-order"
|
||||
],
|
||||
"json": "./.docgen/nhost-js.json",
|
||||
"name": "Nhost JS",
|
||||
"json": "./.docgen/graphql.json",
|
||||
"name": "GraphQL",
|
||||
"readme": "none",
|
||||
"githubPages": false,
|
||||
"cleanOutputDir": false,
|
||||
@@ -26,6 +26,9 @@
|
||||
"@nhost/hasura-storage-js": [
|
||||
"../hasura-storage-js/src/index.ts"
|
||||
],
|
||||
"@nhost/graphql-js": [
|
||||
"../graphql-js/src/index.ts"
|
||||
],
|
||||
"@nhost/nextjs": [
|
||||
"../nextjs/src/index.ts"
|
||||
],
|
||||
65
packages/graphql-js/package.json
Normal file
65
packages/graphql-js/package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "@nhost/graphql-js",
|
||||
"version": "0.0.3",
|
||||
"description": "Nhost GraphQL client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"nhost",
|
||||
"hasura",
|
||||
"graphql"
|
||||
],
|
||||
"author": "Nhost",
|
||||
"homepage": "https://nhost.io",
|
||||
"bugs": "https://github.com/nhost/nhost/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nhost/nhost.git"
|
||||
},
|
||||
"main": "dist/index.cjs.js",
|
||||
"module": "dist/index.esm.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"source": "src/index.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"umd",
|
||||
"README.md"
|
||||
],
|
||||
"exports": {
|
||||
"./package.json": "./package.json",
|
||||
".": {
|
||||
"import": {
|
||||
"node": "./dist/index.cjs.js",
|
||||
"default": "./dist/index.esm.js"
|
||||
},
|
||||
"require": "./dist/index.cjs.js"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite build",
|
||||
"build": "run-p build:lib build:umd",
|
||||
"build:lib": "vite build",
|
||||
"build:umd": "vite build --config ../../config/vite.lib.umd.config.js",
|
||||
"prettier": "prettier --check src/",
|
||||
"prettier:fix": "prettier --write src/",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||
"verify": "run-p prettier lint",
|
||||
"verify:fix": "run-p prettier:fix lint:fix",
|
||||
"typedoc": "typedoc --options ./graphql.typedoc.json --tsconfig ./typedoc.tsconfig.json",
|
||||
"docgen": "pnpm typedoc && docgen --config ./graphql.docgen.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@graphql-typed-document-node/core": "^3.1.1",
|
||||
"cross-fetch": "^3.1.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^14.0.0 || ^15.0.0 || ^16.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nhost/docgen": "workspace:*",
|
||||
"graphql": "16.6.0"
|
||||
}
|
||||
}
|
||||
205
packages/graphql-js/src/client.ts
Normal file
205
packages/graphql-js/src/client.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
import { TypedDocumentNode } from '@graphql-typed-document-node/core'
|
||||
import fetch from 'cross-fetch'
|
||||
import { parseRequestArgs } from './parse-args'
|
||||
import { resolveRequestDocument } from './resolve-request-document'
|
||||
import {
|
||||
NhostGraphqlConstructorParams,
|
||||
NhostGraphqlRequestConfig,
|
||||
NhostGraphqlRequestResponse,
|
||||
RemoveIndex,
|
||||
RequestDocument,
|
||||
RequestOptions,
|
||||
Variables
|
||||
} from './types'
|
||||
|
||||
/**
|
||||
* @alias GraphQL
|
||||
*/
|
||||
export class NhostGraphqlClient {
|
||||
readonly _url: string
|
||||
private accessToken: string | null
|
||||
private adminSecret?: string
|
||||
|
||||
constructor(params: NhostGraphqlConstructorParams) {
|
||||
const { url, adminSecret } = params
|
||||
|
||||
this._url = url
|
||||
this.accessToken = null
|
||||
this.adminSecret = adminSecret
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `nhost.graphql.request` to send a GraphQL request. For more serious GraphQL usage we recommend using a GraphQL client such as Apollo Client (https://www.apollographql.com/docs/react).
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const CUSTOMERS = gql`
|
||||
* query {
|
||||
* customers {
|
||||
* id
|
||||
* name
|
||||
* }
|
||||
* }
|
||||
* `
|
||||
* const { data, error } = await nhost.graphql.request(CUSTOMERS)
|
||||
* ```
|
||||
*
|
||||
* @docs https://docs.nhost.io/reference/javascript/graphql/request
|
||||
*/
|
||||
request<T = any, V = Variables>(
|
||||
document: RequestDocument | TypedDocumentNode<T, V>,
|
||||
...variablesAndRequestHeaders: V extends Record<any, never>
|
||||
? [variables?: V, config?: NhostGraphqlRequestConfig]
|
||||
: keyof RemoveIndex<V> extends never
|
||||
? [variables?: V, config?: NhostGraphqlRequestConfig]
|
||||
: [variables: V, config?: NhostGraphqlRequestConfig]
|
||||
): Promise<NhostGraphqlRequestResponse<T>>
|
||||
async request<T = any, V extends Variables = Variables>(
|
||||
options: RequestOptions<V, T>
|
||||
): Promise<NhostGraphqlRequestResponse<T>>
|
||||
async request<T = any, V extends Variables = Variables>(
|
||||
documentOrOptions: RequestDocument | TypedDocumentNode<T, V> | RequestOptions<V>,
|
||||
...variablesAndRequestHeaders: V extends Record<any, never>
|
||||
? [variables?: V, config?: NhostGraphqlRequestConfig]
|
||||
: keyof RemoveIndex<V> extends never
|
||||
? [variables?: V, config?: NhostGraphqlRequestConfig]
|
||||
: [variables: V, config?: NhostGraphqlRequestConfig]
|
||||
): Promise<NhostGraphqlRequestResponse<T>> {
|
||||
const [variables, config] = variablesAndRequestHeaders
|
||||
const requestOptions = parseRequestArgs(documentOrOptions, variables, config)
|
||||
|
||||
const { headers, ...otherOptions } = config || {}
|
||||
const { query, operationName } = resolveRequestDocument(requestOptions.document)
|
||||
try {
|
||||
const response = await fetch(this.httpUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
operationName,
|
||||
query,
|
||||
variables
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.generateAccessTokenHeaders(),
|
||||
...headers
|
||||
},
|
||||
...otherOptions
|
||||
})
|
||||
if (!response.ok) {
|
||||
return {
|
||||
data: null,
|
||||
error: {
|
||||
error: response.statusText,
|
||||
message: response.statusText,
|
||||
status: response.status
|
||||
}
|
||||
}
|
||||
}
|
||||
const { data, errors } = await response.json()
|
||||
|
||||
if (errors) {
|
||||
return {
|
||||
data: null,
|
||||
error: errors
|
||||
}
|
||||
}
|
||||
if (typeof data !== 'object' || Array.isArray(data) || data === null) {
|
||||
return {
|
||||
data: null,
|
||||
error: {
|
||||
error: 'invalid-response',
|
||||
message: 'incorrect response data from GraphQL server',
|
||||
status: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { data, error: null }
|
||||
} catch (e) {
|
||||
const error = e as Error
|
||||
return {
|
||||
data: null,
|
||||
error: {
|
||||
message: error.message,
|
||||
status: error.name === 'AbortError' ? 0 : 500,
|
||||
error: error.name === 'AbortError' ? 'abort-error' : 'unknown'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `nhost.graphql.httpUrl` to get the GraphQL HTTP URL.
|
||||
* @example
|
||||
* ```ts
|
||||
* const url = nhost.graphql.httpUrl;
|
||||
* ```
|
||||
*
|
||||
* @docs https://docs.nhost.io/reference/javascript/graphql/get-http-url
|
||||
*/
|
||||
get httpUrl(): string {
|
||||
return this._url
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `nhost.graphql.wsUrl` to get the GraphQL WebSocket URL.
|
||||
* @example
|
||||
* ```ts
|
||||
* const url = nhost.graphql.wsUrl;
|
||||
* ```
|
||||
*
|
||||
* @docs https://docs.nhost.io/reference/javascript/graphql/get-ws-url
|
||||
*/
|
||||
get wsUrl(): string {
|
||||
return this._url.replace(/^(http)(s?):\/\//, 'ws$2://')
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `nhost.graphql.url` to get the GraphQL URL.
|
||||
* @deprecated Use `nhost.graphql.httpUrl` and `nhost.graphql.wsUrl` instead.
|
||||
*/
|
||||
get url(): string {
|
||||
return this._url
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `nhost.graphql.getUrl()` to get the GraphQL URL.
|
||||
* @deprecated Use `nhost.graphql.httpUrl` and `nhost.graphql.wsUrl` instead.
|
||||
*/
|
||||
getUrl(): string {
|
||||
return this._url
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `nhost.graphql.setAccessToken` to a set an access token to be used in subsequent graphql requests. Note that if you're signin in users with `nhost.auth.signIn()` the access token will be set automatically.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* nhost.graphql.setAccessToken('some-access-token')
|
||||
* ```
|
||||
*
|
||||
* @docs https://docs.nhost.io/reference/javascript/graphql/set-access-token
|
||||
*/
|
||||
setAccessToken(accessToken: string | undefined) {
|
||||
if (!accessToken) {
|
||||
this.accessToken = null
|
||||
return
|
||||
}
|
||||
|
||||
this.accessToken = accessToken
|
||||
}
|
||||
|
||||
private generateAccessTokenHeaders(): NhostGraphqlRequestConfig['headers'] {
|
||||
if (this.adminSecret) {
|
||||
return {
|
||||
'x-hasura-admin-secret': this.adminSecret
|
||||
}
|
||||
}
|
||||
if (this.accessToken) {
|
||||
return {
|
||||
Authorization: `Bearer ${this.accessToken}`
|
||||
}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
}
|
||||
1
packages/graphql-js/src/index.ts
Normal file
1
packages/graphql-js/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './client'
|
||||
17
packages/graphql-js/src/parse-args.ts
Normal file
17
packages/graphql-js/src/parse-args.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { RequestDocument, RequestOptions, Variables } from './types'
|
||||
|
||||
export function parseRequestArgs<V extends Variables = Variables>(
|
||||
documentOrOptions: RequestDocument | RequestOptions<V>,
|
||||
variables?: V,
|
||||
config?: RequestInit
|
||||
): RequestOptions<V> {
|
||||
return (
|
||||
(documentOrOptions as RequestOptions<V>).document
|
||||
? documentOrOptions
|
||||
: {
|
||||
document: documentOrOptions,
|
||||
variables,
|
||||
config
|
||||
}
|
||||
) as RequestOptions<V>
|
||||
}
|
||||
42
packages/graphql-js/src/resolve-request-document.ts
Normal file
42
packages/graphql-js/src/resolve-request-document.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { DocumentNode, OperationDefinitionNode, parse, print } from 'graphql'
|
||||
import { RequestDocument } from './types'
|
||||
|
||||
/**
|
||||
* helpers
|
||||
*/
|
||||
|
||||
function extractOperationName(document: DocumentNode): string | undefined {
|
||||
let operationName = undefined
|
||||
|
||||
const operationDefinitions = document.definitions.filter(
|
||||
(definition) => definition.kind === 'OperationDefinition'
|
||||
) as OperationDefinitionNode[]
|
||||
|
||||
if (operationDefinitions.length === 1) {
|
||||
operationName = operationDefinitions[0].name?.value
|
||||
}
|
||||
|
||||
return operationName
|
||||
}
|
||||
|
||||
export function resolveRequestDocument(document: RequestDocument): {
|
||||
query: string
|
||||
operationName?: string
|
||||
} {
|
||||
if (typeof document === 'string') {
|
||||
let operationName = undefined
|
||||
|
||||
try {
|
||||
const parsedDocument = parse(document)
|
||||
operationName = extractOperationName(parsedDocument)
|
||||
} catch (err) {
|
||||
// Failed parsing the document, the operationName will be undefined
|
||||
}
|
||||
|
||||
return { query: document, operationName }
|
||||
}
|
||||
|
||||
const operationName = extractOperationName(document)
|
||||
|
||||
return { query: print(document), operationName }
|
||||
}
|
||||
52
packages/graphql-js/src/types.ts
Normal file
52
packages/graphql-js/src/types.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { TypedDocumentNode } from '@graphql-typed-document-node/core'
|
||||
import { DocumentNode, GraphQLError } from 'graphql'
|
||||
|
||||
// TODO shared with other packages
|
||||
export type ErrorPayload = {
|
||||
error: string
|
||||
status: number
|
||||
message: string
|
||||
}
|
||||
|
||||
export type RequestOptions<V extends Variables = Variables, T = any> = NhostGraphqlRequestConfig & {
|
||||
document: RequestDocument | TypedDocumentNode<T, V>
|
||||
} & (V extends Record<any, never>
|
||||
? { variables?: V }
|
||||
: keyof RemoveIndex<V> extends never
|
||||
? { variables?: V }
|
||||
: { variables: V })
|
||||
export type Variables = { [key: string]: any }
|
||||
|
||||
export type RequestDocument = string | DocumentNode
|
||||
|
||||
export type RemoveIndex<T> = {
|
||||
[K in keyof T as string extends K ? never : number extends K ? never : K]: T[K]
|
||||
}
|
||||
|
||||
export interface NhostGraphqlConstructorParams {
|
||||
/**
|
||||
* GraphQL endpoint.
|
||||
*/
|
||||
url: string
|
||||
/**
|
||||
* Admin secret. When set, it is sent as an `x-hasura-admin-secret` header for all requests.
|
||||
*/
|
||||
adminSecret?: string
|
||||
}
|
||||
|
||||
export type NhostGraphqlRequestResponse<T = unknown> =
|
||||
| {
|
||||
data: null
|
||||
error: GraphQLError[] | ErrorPayload
|
||||
}
|
||||
| {
|
||||
data: T
|
||||
error: null
|
||||
}
|
||||
|
||||
/** Subset of RequestInit parameters that are supported by the graphql client */
|
||||
export interface NhostGraphqlRequestConfig {
|
||||
headers?: Record<string, string>
|
||||
/** @deprecated Axios has been replaced by cross-fetch. You should now remove this option. */
|
||||
useAxios?: false
|
||||
}
|
||||
4
packages/graphql-js/tsconfig.json
Normal file
4
packages/graphql-js/tsconfig.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "../../config/tsconfig.base.json",
|
||||
"include": ["src"]
|
||||
}
|
||||
7
packages/graphql-js/tsconfig.test.json
Normal file
7
packages/graphql-js/tsconfig.test.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"include": [
|
||||
"tests/types"
|
||||
]
|
||||
}
|
||||
13
packages/graphql-js/vite.config.e2e.js
Normal file
13
packages/graphql-js/vite.config.e2e.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
import baseConfig from '../../config/vite.lib.config'
|
||||
|
||||
export default defineConfig({
|
||||
...baseConfig,
|
||||
test: {
|
||||
...(baseConfig.test || {}),
|
||||
include: [`tests/e2e/**/*.{spec,test}.{ts,tsx}`],
|
||||
testTimeout: 30000,
|
||||
environment: 'node'
|
||||
}
|
||||
})
|
||||
5
packages/graphql-js/vite.config.js
Normal file
5
packages/graphql-js/vite.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
import baseConfig from '../../config/vite.lib.config'
|
||||
|
||||
export default defineConfig(baseConfig)
|
||||
13
packages/graphql-js/vite.config.unit.js
Normal file
13
packages/graphql-js/vite.config.unit.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
import baseConfig from '../../config/vite.lib.config'
|
||||
|
||||
export default defineConfig({
|
||||
...baseConfig,
|
||||
test: {
|
||||
...(baseConfig.test || {}),
|
||||
include: [`tests/unit/**/*.{spec,test}.{ts,tsx}`],
|
||||
testTimeout: 30000,
|
||||
environment: 'node'
|
||||
}
|
||||
})
|
||||
@@ -1,5 +1,41 @@
|
||||
# @nhost/hasura-auth-js
|
||||
|
||||
## 2.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 19b11d40: Remove the deprecated `AuthCookieClient` and `AuthClientSSR` constructors
|
||||
|
||||
Use the `clientStorageType` option instead:
|
||||
|
||||
```ts
|
||||
const nhost = new NhostClient({ clientStorageType: 'cookie' })
|
||||
```
|
||||
|
||||
- 19b11d40: Remove the deprecated `nhost.auth.getJWTToken` method
|
||||
|
||||
Use `nhost.auth.getAccessToken()` instead.
|
||||
|
||||
- 19b11d40: Remove the deprecated `autoLogin` option
|
||||
|
||||
Use `autoSignIn` instead:
|
||||
|
||||
```ts
|
||||
const nhost = new NhostClient({ autoSignIn: true })
|
||||
```
|
||||
|
||||
- 19b11d40: Remove the deprecated `clientStorageGetter` and `clientStorageSetter` options
|
||||
|
||||
Use `clientStorageType` and `clientStorage` instead:
|
||||
|
||||
```ts
|
||||
const nhost = new NhostClient({ clientStorageType: 'custom', clientStorage: TODO })
|
||||
```
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 80bbd3a1: Replace `axios` by `cross-fetch`
|
||||
|
||||
## 1.12.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { faker } from '@faker-js/faker'
|
||||
import axios from 'axios'
|
||||
import fetch from 'cross-fetch'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { auth, getHtmlLink, mailhog } from './helpers'
|
||||
|
||||
describe('emails', () => {
|
||||
@@ -22,10 +21,11 @@ describe('emails', () => {
|
||||
const verifyEmailLink = await getHtmlLink(email, 'verifyEmail')
|
||||
|
||||
// verify email
|
||||
await axios.get(verifyEmailLink, {
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) => status === 302
|
||||
})
|
||||
try {
|
||||
await fetch(verifyEmailLink, { method: 'GET', redirect: 'follow' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const signInA = await auth.signIn({
|
||||
email,
|
||||
@@ -46,10 +46,11 @@ describe('emails', () => {
|
||||
const changeEmailLink = await getHtmlLink(email, 'emailConfirmChange')
|
||||
|
||||
// verify email
|
||||
await axios.get(changeEmailLink, {
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) => status === 302
|
||||
})
|
||||
try {
|
||||
await fetch(changeEmailLink, { method: 'GET', redirect: 'follow' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
|
||||
it('reset email verification', async () => {
|
||||
@@ -71,28 +72,21 @@ describe('emails', () => {
|
||||
expect(signInA.error).toBeTruthy()
|
||||
expect(signInA.session).toBeNull()
|
||||
|
||||
await mailhog.deleteAll()
|
||||
|
||||
await auth.sendVerificationEmail({ email })
|
||||
|
||||
// make sure onle a single message exists
|
||||
const messages = await mailhog.messages()
|
||||
|
||||
if (!messages) {
|
||||
throw new Error('no messages')
|
||||
}
|
||||
|
||||
expect(messages.count).toBe(1)
|
||||
const message = await mailhog.latestTo(email)
|
||||
expect(message?.subject).toBe('Verify your email')
|
||||
|
||||
// test email link
|
||||
// get verify email link
|
||||
const verifyEmailLink = await getHtmlLink(email, 'verifyEmail')
|
||||
|
||||
// verify email
|
||||
await axios.get(verifyEmailLink, {
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) => status === 302
|
||||
})
|
||||
try {
|
||||
await fetch(verifyEmailLink, { method: 'GET', redirect: 'follow' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// sign in should work
|
||||
const signInB = await auth.signIn({
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import axios from 'axios'
|
||||
import { load } from 'cheerio'
|
||||
import fetch from 'cross-fetch'
|
||||
import createMailhogClient from 'mailhog'
|
||||
import { expect } from 'vitest'
|
||||
|
||||
import { HasuraAuthClient, SignUpParams } from '../src'
|
||||
|
||||
const AUTH_BACKEND_URL = 'http://localhost:1337/v1/auth'
|
||||
@@ -43,11 +42,11 @@ export const signUpAndVerifyUser = async (params: SignUpParams) => {
|
||||
// get verify email link
|
||||
const verifyEmailLink = await getHtmlLink(email, 'verifyEmail')
|
||||
|
||||
// verify email
|
||||
await axios.get(verifyEmailLink, {
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) => status === 302
|
||||
})
|
||||
try {
|
||||
await fetch(verifyEmailLink, { method: 'GET', redirect: 'follow' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export const signUpAndInUser = async (params: SignUpParams) => {
|
||||
@@ -60,10 +59,11 @@ export const signUpAndInUser = async (params: SignUpParams) => {
|
||||
const verifyEmailLink = await getHtmlLink(email, 'verifyEmail')
|
||||
|
||||
// verify email
|
||||
await axios.get(verifyEmailLink, {
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) => status === 302
|
||||
})
|
||||
try {
|
||||
await fetch(verifyEmailLink, { method: 'GET', redirect: 'follow' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// sign in
|
||||
const { session, error } = await auth.signIn({ email, password })
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { faker } from '@faker-js/faker'
|
||||
import axios from 'axios'
|
||||
import fetch from 'cross-fetch'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { auth, getHtmlLink, signUpAndInUser, signUpAndVerifyUser } from './helpers'
|
||||
|
||||
describe('passwords', () => {
|
||||
@@ -48,9 +47,10 @@ describe('passwords', () => {
|
||||
const resetPasswordLink = await getHtmlLink(email, 'passwordReset')
|
||||
|
||||
// verify email
|
||||
await axios.get(resetPasswordLink, {
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) => status === 302
|
||||
})
|
||||
try {
|
||||
await fetch(resetPasswordLink, { method: 'GET', redirect: 'follow' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { faker } from '@faker-js/faker'
|
||||
import axios from 'axios'
|
||||
import fetch from 'cross-fetch'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { USER_ALREADY_SIGNED_IN } from '../src'
|
||||
|
||||
import { auth, getHtmlLink, signUpAndInUser, signUpAndVerifyUser } from './helpers'
|
||||
|
||||
describe('sign-in', () => {
|
||||
@@ -57,10 +56,11 @@ describe('sign-in', () => {
|
||||
const emailLink = await getHtmlLink(email, 'signinPasswordless')
|
||||
|
||||
// verify email
|
||||
await axios.get(emailLink, {
|
||||
maxRedirects: 0,
|
||||
validateStatus: (status) => status === 302
|
||||
})
|
||||
try {
|
||||
await fetch(emailLink, { method: 'GET', redirect: 'follow' })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})
|
||||
|
||||
it('should not be possible to sign in with email+password in when already authenticated', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/hasura-auth-js",
|
||||
"version": "1.12.4",
|
||||
"version": "2.0.0",
|
||||
"description": "Hasura-auth client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -63,7 +63,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@simplewebauthn/browser": "^6.0.0",
|
||||
"axios": "^1.2.0",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"js-cookie": "^3.0.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"xstate": "^4.33.5"
|
||||
|
||||
@@ -75,11 +75,8 @@ export class HasuraAuthClient {
|
||||
url,
|
||||
autoRefreshToken = true,
|
||||
autoSignIn = true,
|
||||
autoLogin,
|
||||
clientStorage,
|
||||
clientStorageType,
|
||||
clientStorageGetter,
|
||||
clientStorageSetter,
|
||||
refreshIntervalTime,
|
||||
start = true
|
||||
}: NhostAuthConstructorParams) {
|
||||
@@ -88,12 +85,10 @@ export class HasuraAuthClient {
|
||||
backendUrl: url,
|
||||
clientUrl: (typeof window !== 'undefined' && window.location?.origin) || '',
|
||||
autoRefreshToken,
|
||||
autoSignIn: typeof autoLogin === 'boolean' ? autoLogin : autoSignIn,
|
||||
autoSignIn,
|
||||
start,
|
||||
clientStorage,
|
||||
clientStorageType,
|
||||
clientStorageGetter,
|
||||
clientStorageSetter,
|
||||
refreshIntervalTime
|
||||
})
|
||||
}
|
||||
@@ -552,16 +547,6 @@ export class HasuraAuthClient {
|
||||
return { isAuthenticated: this.isAuthenticated(), isLoading: false, connectionAttempts }
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @deprecated Use `nhost.auth.getAccessToken()` instead.
|
||||
* @docs https://docs.nhost.io/reference/javascript/auth/get-access-token
|
||||
*/
|
||||
|
||||
getJWTToken(): string | undefined {
|
||||
return this.getAccessToken()
|
||||
}
|
||||
|
||||
/**
|
||||
* Use `nhost.auth.getAccessToken` to get the access token of the user.
|
||||
*
|
||||
|
||||
@@ -5,7 +5,6 @@ import type {
|
||||
PublicKeyCredentialRequestOptionsJSON,
|
||||
RegistrationCredentialJSON
|
||||
} from '@simplewebauthn/typescript-types'
|
||||
import type { AxiosRequestConfig } from 'axios'
|
||||
import { assign, createMachine, InterpreterFrom, send } from 'xstate'
|
||||
import {
|
||||
NHOST_JWT_EXPIRES_AT_KEY,
|
||||
@@ -42,16 +41,14 @@ import {
|
||||
} from '../../types'
|
||||
import {
|
||||
getParameterByName,
|
||||
nhostApiClient,
|
||||
removeParameterFromWindow,
|
||||
rewriteRedirectTo
|
||||
} from '../../utils'
|
||||
import {
|
||||
isValidEmail,
|
||||
isValidPassword,
|
||||
isValidPhoneNumber,
|
||||
isValidTicket
|
||||
} from '../../utils/validators'
|
||||
isValidTicket,
|
||||
postFetch,
|
||||
removeParameterFromWindow,
|
||||
rewriteRedirectTo
|
||||
} from '../../utils'
|
||||
import { AuthContext, INITIAL_MACHINE_CONTEXT } from './context'
|
||||
import { AuthEvents } from './events'
|
||||
|
||||
@@ -81,23 +78,20 @@ type AuthServices = {
|
||||
export const createAuthMachine = ({
|
||||
backendUrl,
|
||||
clientUrl,
|
||||
clientStorageGetter,
|
||||
clientStorageSetter,
|
||||
clientStorageType = 'web',
|
||||
clientStorage,
|
||||
refreshIntervalTime,
|
||||
autoRefreshToken = true,
|
||||
autoSignIn = true
|
||||
}: AuthMachineOptions) => {
|
||||
const storageGetter = clientStorageGetter || localStorageGetter(clientStorageType, clientStorage)
|
||||
const storageSetter = clientStorageSetter || localStorageSetter(clientStorageType, clientStorage)
|
||||
const api = nhostApiClient(backendUrl)
|
||||
const storageGetter = localStorageGetter(clientStorageType, clientStorage)
|
||||
const storageSetter = localStorageSetter(clientStorageType, clientStorage)
|
||||
const postRequest = async <T = any, D = any>(
|
||||
url: string,
|
||||
data?: D,
|
||||
config?: AxiosRequestConfig<D>
|
||||
token?: string | null
|
||||
): Promise<T> => {
|
||||
const result = await api.post(url, data, config)
|
||||
const result = await postFetch<T>(`${backendUrl}${url}`, data, token)
|
||||
|
||||
return result.data
|
||||
}
|
||||
@@ -704,11 +698,7 @@ export const createAuthMachine = ({
|
||||
phoneNumber,
|
||||
options: rewriteRedirectTo(clientUrl, options)
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${context.accessToken.value}`
|
||||
}
|
||||
}
|
||||
context.accessToken.value
|
||||
)
|
||||
} else {
|
||||
return postRequest('/signin/passwordless/sms', {
|
||||
@@ -739,11 +729,7 @@ export const createAuthMachine = ({
|
||||
email,
|
||||
options: rewriteRedirectTo(clientUrl, options)
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${context.accessToken.value}`
|
||||
}
|
||||
}
|
||||
context.accessToken.value
|
||||
)
|
||||
} else {
|
||||
return postRequest('/signin/passwordless/email', {
|
||||
@@ -811,11 +797,7 @@ export const createAuthMachine = ({
|
||||
password,
|
||||
options: rewriteRedirectTo(clientUrl, options)
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${context.accessToken.value}`
|
||||
}
|
||||
}
|
||||
context.accessToken.value
|
||||
)
|
||||
} else {
|
||||
return postRequest<SignUpResponse>('/signup/email-password', {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { assign, createMachine, send } from 'xstate'
|
||||
|
||||
import { INVALID_EMAIL_ERROR } from '../errors'
|
||||
import { AuthClient } from '../internal-client'
|
||||
import { ChangeEmailOptions, ChangeEmailResponse, ErrorPayload } from '../types'
|
||||
import { nhostApiClient, rewriteRedirectTo } from '../utils'
|
||||
import { postFetch, rewriteRedirectTo } from '../utils'
|
||||
import { isValidEmail } from '../utils/validators'
|
||||
|
||||
export type ChangeEmailContext = {
|
||||
@@ -26,7 +25,6 @@ export type ChangeEmailServices = {
|
||||
export type ChangeEmailMachine = ReturnType<typeof createChangeEmailMachine>
|
||||
|
||||
export const createChangeEmailMachine = ({ backendUrl, clientUrl, interpreter }: AuthClient) => {
|
||||
const api = nhostApiClient(backendUrl)
|
||||
return createMachine(
|
||||
{
|
||||
schema: {
|
||||
@@ -86,17 +84,10 @@ export const createChangeEmailMachine = ({ backendUrl, clientUrl, interpreter }:
|
||||
},
|
||||
services: {
|
||||
requestChange: async (_, { email, options }) => {
|
||||
const res = await api.post(
|
||||
'/user/email/change',
|
||||
{
|
||||
newEmail: email,
|
||||
options: rewriteRedirectTo(clientUrl, options)
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${interpreter?.getSnapshot().context.accessToken.value}`
|
||||
}
|
||||
}
|
||||
const res = await postFetch(
|
||||
`${backendUrl}/user/email/change`,
|
||||
{ newEmail: email, options: rewriteRedirectTo(clientUrl, options) },
|
||||
interpreter?.getSnapshot().context.accessToken.value
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { assign, createMachine, send } from 'xstate'
|
||||
|
||||
import { INVALID_PASSWORD_ERROR } from '../errors'
|
||||
import { AuthClient } from '../internal-client'
|
||||
import { ChangePasswordResponse, ErrorPayload } from '../types'
|
||||
import { nhostApiClient } from '../utils'
|
||||
import { postFetch } from '../utils'
|
||||
import { isValidPassword } from '../utils/validators'
|
||||
|
||||
export type ChangePasswordContext = {
|
||||
@@ -25,7 +24,6 @@ export type ChangePasswordServices = {
|
||||
export type ChangePasswordMachine = ReturnType<typeof createChangePasswordMachine>
|
||||
|
||||
export const createChangePasswordMachine = ({ backendUrl, interpreter }: AuthClient) => {
|
||||
const api = nhostApiClient(backendUrl)
|
||||
return createMachine(
|
||||
{
|
||||
schema: {
|
||||
@@ -84,14 +82,10 @@ export const createChangePasswordMachine = ({ backendUrl, interpreter }: AuthCli
|
||||
},
|
||||
services: {
|
||||
requestChange: (_, { password, ticket }) =>
|
||||
api.post<string, ChangePasswordResponse>(
|
||||
'/user/password',
|
||||
postFetch<ChangePasswordResponse>(
|
||||
`${backendUrl}/user/password`,
|
||||
{ newPassword: password, ticket: ticket },
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${interpreter?.getSnapshot().context.accessToken.value}`
|
||||
}
|
||||
}
|
||||
interpreter?.getSnapshot().context.accessToken.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { assign, createMachine, send } from 'xstate'
|
||||
|
||||
import { INVALID_MFA_CODE_ERROR, INVALID_MFA_TYPE_ERROR } from '../errors'
|
||||
import { AuthClient } from '../internal-client'
|
||||
import { ErrorPayload } from '../types'
|
||||
import { nhostApiClient } from '../utils'
|
||||
import { getFetch, postFetch } from '../utils'
|
||||
|
||||
export type EnableMfaContext = {
|
||||
error: ErrorPayload | null
|
||||
@@ -28,7 +27,6 @@ export type EnableMfaEvents =
|
||||
export type EnableMfadMachine = ReturnType<typeof createEnableMfaMachine>
|
||||
|
||||
export const createEnableMfaMachine = ({ backendUrl, interpreter }: AuthClient) => {
|
||||
const api = nhostApiClient(backendUrl)
|
||||
return createMachine(
|
||||
{
|
||||
schema: {
|
||||
@@ -107,7 +105,10 @@ export const createEnableMfaMachine = ({ backendUrl, interpreter }: AuthClient)
|
||||
imageUrl: (_, { data: { imageUrl } }: any) => imageUrl,
|
||||
secret: (_, { data: { totpSecret } }: any) => totpSecret
|
||||
}),
|
||||
reportError: send((ctx) => ({ type: 'ERROR', error: ctx.error })),
|
||||
reportError: send((ctx, event) => {
|
||||
console.log('REPORT', ctx, event)
|
||||
return { type: 'ERROR', error: ctx.error }
|
||||
}),
|
||||
reportSuccess: send('SUCCESS'),
|
||||
reportGeneratedSuccess: send('GENERATED'),
|
||||
reportGeneratedError: send((ctx) => ({ type: 'GENERATED_ERROR', error: ctx.error }))
|
||||
@@ -118,25 +119,17 @@ export const createEnableMfaMachine = ({ backendUrl, interpreter }: AuthClient)
|
||||
},
|
||||
services: {
|
||||
generate: async (_) => {
|
||||
const { data } = await api.get('/mfa/totp/generate', {
|
||||
headers: {
|
||||
authorization: `Bearer ${interpreter?.getSnapshot().context.accessToken.value}`
|
||||
}
|
||||
})
|
||||
const { data } = await getFetch(
|
||||
`${backendUrl}/mfa/totp/generate`,
|
||||
interpreter?.getSnapshot().context.accessToken.value
|
||||
)
|
||||
return data
|
||||
},
|
||||
activate: (_, { code, activeMfaType }) =>
|
||||
api.post(
|
||||
'/user/mfa',
|
||||
{
|
||||
code,
|
||||
activeMfaType
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${interpreter?.getSnapshot().context.accessToken.value}`
|
||||
}
|
||||
}
|
||||
postFetch(
|
||||
`${backendUrl}/user/mfa`,
|
||||
{ code, activeMfaType },
|
||||
interpreter?.getSnapshot().context.accessToken.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { assign, createMachine, send } from 'xstate'
|
||||
|
||||
import { INVALID_EMAIL_ERROR } from '../errors'
|
||||
import { AuthClient } from '../internal-client'
|
||||
import { ErrorPayload, ResetPasswordOptions, ResetPasswordResponse } from '../types'
|
||||
import { nhostApiClient, rewriteRedirectTo } from '../utils'
|
||||
import { postFetch, rewriteRedirectTo } from '../utils'
|
||||
import { isValidEmail } from '../utils/validators'
|
||||
|
||||
export type ResetPasswordContext = {
|
||||
@@ -25,7 +24,6 @@ export type ResetPasswordServices = {
|
||||
export type ResetPasswordMachine = ReturnType<typeof createResetPasswordMachine>
|
||||
|
||||
export const createResetPasswordMachine = ({ backendUrl, clientUrl }: AuthClient) => {
|
||||
const api = nhostApiClient(backendUrl)
|
||||
return createMachine(
|
||||
{
|
||||
schema: {
|
||||
@@ -84,7 +82,7 @@ export const createResetPasswordMachine = ({ backendUrl, clientUrl }: AuthClient
|
||||
},
|
||||
services: {
|
||||
requestChange: (_, { email, options }) =>
|
||||
api.post<string, ResetPasswordResponse>('/user/password/reset', {
|
||||
postFetch<ResetPasswordResponse>(`${backendUrl}/user/password/reset`, {
|
||||
email,
|
||||
options: rewriteRedirectTo(clientUrl, options)
|
||||
})
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { assign, createMachine, send } from 'xstate'
|
||||
|
||||
import { INVALID_EMAIL_ERROR } from '../errors'
|
||||
import { AuthClient } from '../internal-client'
|
||||
import { ErrorPayload, SendVerificationEmailOptions, SendVerificationEmailResponse } from '../types'
|
||||
import { nhostApiClient, rewriteRedirectTo } from '../utils'
|
||||
import { postFetch, rewriteRedirectTo } from '../utils'
|
||||
import { isValidEmail } from '../utils/validators'
|
||||
|
||||
export type SendVerificationEmailContext = {
|
||||
@@ -25,7 +24,6 @@ export type SendVerificationEmailServices = {
|
||||
|
||||
export type SendVerificationEmailMachine = ReturnType<typeof createSendVerificationEmailMachine>
|
||||
export const createSendVerificationEmailMachine = ({ backendUrl, clientUrl }: AuthClient) => {
|
||||
const api = nhostApiClient(backendUrl)
|
||||
return createMachine(
|
||||
{
|
||||
schema: {
|
||||
@@ -84,12 +82,9 @@ export const createSendVerificationEmailMachine = ({ backendUrl, clientUrl }: Au
|
||||
},
|
||||
services: {
|
||||
request: async (_, { email, options }) => {
|
||||
const res = await api.post<SendVerificationEmailResponse>(
|
||||
'/user/email/send-verification-email',
|
||||
{
|
||||
email,
|
||||
options: rewriteRedirectTo(clientUrl, options)
|
||||
}
|
||||
const res = await postFetch<SendVerificationEmailResponse>(
|
||||
`${backendUrl}/user/email/send-verification-email`,
|
||||
{ email, options: rewriteRedirectTo(clientUrl, options) }
|
||||
)
|
||||
return res.data
|
||||
}
|
||||
|
||||
@@ -3,13 +3,12 @@ import {
|
||||
PublicKeyCredentialCreationOptionsJSON,
|
||||
RegistrationCredentialJSON
|
||||
} from '@simplewebauthn/typescript-types'
|
||||
|
||||
import { postFetch } from '..'
|
||||
import { CodifiedError } from '../errors'
|
||||
import { AuthClient } from '../internal-client'
|
||||
import { ErrorPayload, SecurityKey } from '../types'
|
||||
import { nhostApiClient } from '../utils'
|
||||
|
||||
import { ActionErrorState, ActionLoadingState, ActionSuccessState } from './types'
|
||||
|
||||
export interface AddSecurityKeyHandlerResult extends ActionErrorState, ActionSuccessState {
|
||||
key?: SecurityKey
|
||||
}
|
||||
@@ -20,16 +19,11 @@ export const addSecurityKeyPromise = async (
|
||||
{ backendUrl, interpreter }: AuthClient,
|
||||
nickname?: string
|
||||
): Promise<AddSecurityKeyHandlerResult> => {
|
||||
const api = nhostApiClient(backendUrl)
|
||||
try {
|
||||
const { data: options } = await api.post<PublicKeyCredentialCreationOptionsJSON>(
|
||||
const { data: options } = await postFetch<PublicKeyCredentialCreationOptionsJSON>(
|
||||
'/user/webauthn/add',
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${interpreter?.getSnapshot().context.accessToken.value}`
|
||||
}
|
||||
}
|
||||
interpreter?.getSnapshot().context.accessToken.value
|
||||
)
|
||||
let credential: RegistrationCredentialJSON
|
||||
try {
|
||||
@@ -37,14 +31,10 @@ export const addSecurityKeyPromise = async (
|
||||
} catch (e) {
|
||||
throw new CodifiedError(e as Error)
|
||||
}
|
||||
const { data: key } = await api.post<SecurityKey>(
|
||||
'/user/webauthn/verify',
|
||||
const { data: key } = await postFetch<SecurityKey>(
|
||||
`${backendUrl}/user/webauthn/verify`,
|
||||
{ credential, nickname },
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${interpreter?.getSnapshot().context.accessToken.value}`
|
||||
}
|
||||
}
|
||||
interpreter?.getSnapshot().context.accessToken.value
|
||||
)
|
||||
return { key, isError: false, error: null, isSuccess: true }
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ErrorPayload, NhostSession } from './common'
|
||||
|
||||
// Hasura-auth API response types
|
||||
interface NullableErrorResponse {
|
||||
export interface NullableErrorResponse {
|
||||
error: ErrorPayload | null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import axios, { AxiosError } from 'axios'
|
||||
|
||||
import { NETWORK_ERROR_CODE } from '../errors'
|
||||
import { ErrorPayload } from '../types'
|
||||
|
||||
export const nhostApiClient = (backendUrl: string) => {
|
||||
const client = axios.create({ baseURL: backendUrl })
|
||||
|
||||
client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError<{ message: string; error?: string; statusCode?: number }>) =>
|
||||
Promise.reject<{ error: ErrorPayload }>({
|
||||
error: {
|
||||
message:
|
||||
error.response?.data?.message ??
|
||||
error.message ??
|
||||
error.request.responseText ??
|
||||
JSON.stringify(error),
|
||||
status: error.response?.status ?? error.response?.data?.statusCode ?? NETWORK_ERROR_CODE,
|
||||
error: error.response?.data?.error || error.request.statusText || 'network'
|
||||
}
|
||||
})
|
||||
)
|
||||
return client
|
||||
}
|
||||
58
packages/hasura-auth-js/src/utils/fetch.ts
Normal file
58
packages/hasura-auth-js/src/utils/fetch.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import fetch from 'cross-fetch'
|
||||
import { NETWORK_ERROR_CODE } from '../errors'
|
||||
import { NullableErrorResponse } from '../types'
|
||||
|
||||
interface FetcResponse<T> extends NullableErrorResponse {
|
||||
data: T
|
||||
}
|
||||
|
||||
const fetchWrapper = async <T>(
|
||||
url: string,
|
||||
method: 'GET' | 'POST',
|
||||
{ token, body }: { token?: string | null; body?: any } = {}
|
||||
): Promise<FetcResponse<T>> => {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: '*/*'
|
||||
}
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
const options: RequestInit = {
|
||||
method,
|
||||
headers
|
||||
}
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body)
|
||||
}
|
||||
try {
|
||||
const result = await fetch(url, options)
|
||||
if (!result.ok) {
|
||||
const error = await result.json()
|
||||
return Promise.reject<FetcResponse<T>>({ error })
|
||||
}
|
||||
try {
|
||||
const data = await result.json()
|
||||
return { data, error: null }
|
||||
} catch {
|
||||
console.warn(`Unexpected response: can't parse the response of the server at ${url}`)
|
||||
return { data: 'OK' as any, error: null }
|
||||
}
|
||||
} catch (e) {
|
||||
const error = {
|
||||
message: 'Network Error',
|
||||
status: NETWORK_ERROR_CODE,
|
||||
error: 'network'
|
||||
}
|
||||
return Promise.reject<FetcResponse<T>>({ error })
|
||||
}
|
||||
}
|
||||
|
||||
export const postFetch = async <T>(
|
||||
url: string,
|
||||
body: any,
|
||||
token?: string | null
|
||||
): Promise<FetcResponse<T>> => fetchWrapper<T>(url, 'POST', { token, body })
|
||||
|
||||
export const getFetch = <T>(url: string, token?: string | null): Promise<FetcResponse<T>> =>
|
||||
fetchWrapper<T>(url, 'GET', { token })
|
||||
@@ -1,5 +1,5 @@
|
||||
export * from './axios'
|
||||
export * from './client-helpers'
|
||||
export * from './environment'
|
||||
export * from './fetch'
|
||||
export * from './url'
|
||||
export * from './validators'
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
invalidDeamonymisationEmailError
|
||||
} from './helpers/handlers'
|
||||
import { fakeAnonymousUser } from './helpers/mocks/user'
|
||||
|
||||
import server from './helpers/server'
|
||||
import CustomClientStorage from './helpers/storage'
|
||||
|
||||
@@ -51,7 +50,7 @@ describe('Anonymous Sign-in', () => {
|
||||
expect(state.context.errors).toMatchInlineSnapshot(`
|
||||
{
|
||||
"authentication": {
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
},
|
||||
|
||||
@@ -61,7 +61,7 @@ test(`should fail if there is a network error`, async () => {
|
||||
|
||||
expect(state.context.error).toMatchInlineSnapshot(`
|
||||
{
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ test(`should fail if there is a network error`, async () => {
|
||||
|
||||
expect(state.context.error).toMatchInlineSnapshot(`
|
||||
{
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ test(`should fail if network is unavailable`, async () => {
|
||||
expect(state.context.errors).toMatchInlineSnapshot(`
|
||||
{
|
||||
"registration": {
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
},
|
||||
|
||||
@@ -68,7 +68,7 @@ describe(`Generation`, () => {
|
||||
|
||||
expect(state.context.error).toMatchInlineSnapshot(`
|
||||
{
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const correctAnonymousHandler = rest.post(
|
||||
export const deamonymisationSuccessfulHandler = rest.post(
|
||||
`${BASE_URL}/user/deanonymize`,
|
||||
(_req, res, ctx) => {
|
||||
return res(ctx.status(200))
|
||||
return res(ctx.status(200), ctx.json('OK'))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -48,6 +48,6 @@ export const changeEmailUnauthorizedErrorHandler = rest.post(
|
||||
export const changeEmailSuccessHandler = rest.post(
|
||||
`${BASE_URL}/user/email/change`,
|
||||
(_req, res, ctx) => {
|
||||
return res(ctx.status(200))
|
||||
return res(ctx.status(200), ctx.json('OK'))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -48,6 +48,6 @@ export const changePasswordUnauthorizedErrorHandler = rest.post(
|
||||
export const changePasswordSuccessHandler = rest.post(
|
||||
`${BASE_URL}/user/password`,
|
||||
(_req, res, ctx) => {
|
||||
return res(ctx.status(200))
|
||||
return res(ctx.status(200), ctx.json('OK'))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -101,5 +101,5 @@ export const activateMfaTotpUnauthorizedErrorHandler = rest.post(
|
||||
* Request handler for MSW to mock an successful network request when activating MFA.
|
||||
*/
|
||||
export const activateMfaTotpSuccessHandler = rest.post(`${BASE_URL}/user/mfa`, (_req, res, ctx) => {
|
||||
return res(ctx.status(200))
|
||||
return res(ctx.status(200), ctx.json('OK'))
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import { BASE_URL } from '../config'
|
||||
export const correctPasswordlessEmailHandler = rest.post(
|
||||
`${BASE_URL}/signin/passwordless/email`,
|
||||
(_req, res, ctx) => {
|
||||
return res(ctx.status(200))
|
||||
return res(ctx.status(200), ctx.json('OK'))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import fakeUser from '../mocks/user'
|
||||
export const correctPasswordlessSmsHandler = rest.post(
|
||||
`${BASE_URL}/signin/passwordless/sms`,
|
||||
(_req, res, ctx) => {
|
||||
return res(ctx.status(200))
|
||||
return res(ctx.status(200), ctx.json('OK'))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -64,6 +64,6 @@ export const resetPasswordUserNotFoundHandler = rest.post(
|
||||
export const resetPasswordSuccessHandler = rest.post(
|
||||
`${BASE_URL}/user/password/reset`,
|
||||
(_req, res, ctx) => {
|
||||
return res(ctx.status(200))
|
||||
return res(ctx.status(200), ctx.json('OK'))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -64,6 +64,6 @@ export const sendVerificationEmailUserNotFoundHandler = rest.post(
|
||||
export const sendVerificationEmailSuccessHandler = rest.post(
|
||||
`${BASE_URL}/user/email/send-verification-email`,
|
||||
(_req, res, ctx) => {
|
||||
return res(ctx.status(200))
|
||||
return res(ctx.status(200), ctx.json('OK'))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ export const signOutHandler = rest.post(
|
||||
)
|
||||
}
|
||||
|
||||
return res(ctx.status(200))
|
||||
return res(ctx.status(200), ctx.json('OK'))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ test(`should fail if network is unavailable`, async () => {
|
||||
expect(state.context.errors).toMatchInlineSnapshot(`
|
||||
{
|
||||
"authentication": {
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
},
|
||||
|
||||
@@ -57,7 +57,7 @@ test(`should fail if network is unavailable`, async () => {
|
||||
expect(state.context.errors).toMatchInlineSnapshot(`
|
||||
{
|
||||
"authentication": {
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
},
|
||||
@@ -81,7 +81,7 @@ test(`should fail if server returns an error`, async () => {
|
||||
expect(state.context.errors).toMatchInlineSnapshot(`
|
||||
{
|
||||
"authentication": {
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ test('should fail if network is unavailable', async () => {
|
||||
expect(state.context.errors).toMatchInlineSnapshot(`
|
||||
{
|
||||
"registration": {
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ test(`should fail if network is unavailable`, async () => {
|
||||
expect(state.context.errors).toMatchInlineSnapshot(`
|
||||
{
|
||||
"registration": {
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
},
|
||||
|
||||
@@ -340,7 +340,7 @@ describe('General and disabled auto-sign in', () => {
|
||||
expect(state.context.errors).toMatchInlineSnapshot(`
|
||||
{
|
||||
"authentication": {
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
},
|
||||
|
||||
@@ -49,7 +49,7 @@ test(`should fail if there is a network error`, async () => {
|
||||
|
||||
expect(state.context.error).toMatchInlineSnapshot(`
|
||||
{
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
}
|
||||
|
||||
@@ -3,9 +3,7 @@ import { AuthenticationCredentialJSON } from '@simplewebauthn/typescript-types'
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, expect, test, vi } from 'vitest'
|
||||
import { interpret } from 'xstate'
|
||||
import { waitFor } from 'xstate/lib/waitFor'
|
||||
|
||||
import { createAuthMachine } from '../src'
|
||||
|
||||
import { BASE_URL } from './helpers/config'
|
||||
import {
|
||||
authTokenNetworkErrorHandler,
|
||||
@@ -78,7 +76,7 @@ test(`should fail if network is unavailable`, async () => {
|
||||
expect(state.context.errors).toMatchInlineSnapshot(`
|
||||
{
|
||||
"authentication": {
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
},
|
||||
@@ -99,7 +97,7 @@ test(`should fail if server returns an error`, async () => {
|
||||
expect(state.context.errors).toMatchInlineSnapshot(`
|
||||
{
|
||||
"authentication": {
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
},
|
||||
|
||||
@@ -80,7 +80,7 @@ describe('Security Key', () => {
|
||||
expect(state.context.errors).toMatchInlineSnapshot(`
|
||||
{
|
||||
"registration": {
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
},
|
||||
|
||||
@@ -51,7 +51,7 @@ test(`should fail if there is a network error`, async () => {
|
||||
|
||||
expect(state.context.error).toMatchInlineSnapshot(`
|
||||
{
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ test(`should fail if network is unavailable`, async () => {
|
||||
expect(state.context.errors).toMatchInlineSnapshot(`
|
||||
{
|
||||
"authentication": {
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
},
|
||||
|
||||
@@ -53,7 +53,7 @@ test(`should fail if network is unavailable`, async () => {
|
||||
expect(state.context.errors).toMatchInlineSnapshot(`
|
||||
{
|
||||
"registration": {
|
||||
"error": "OK",
|
||||
"error": "network",
|
||||
"message": "Network Error",
|
||||
"status": 0,
|
||||
},
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
# @nhost/hasura-storage-js
|
||||
|
||||
## 2.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- 19b11d40: Remove the deprecated `nhost.storage.getUrl` method
|
||||
|
||||
Use `nhost.storage.getPublicUrl` instead.
|
||||
|
||||
- 80bbd3a1: Replace `axios` by `cross-fetch`
|
||||
|
||||
`@nhost/hasura-storage-js` now uses `cross-fetch` instead of `axios`.
|
||||
When in a browser, it uploads files using `XMLHttpRequest` to be able to track upload progress (feature available in React and Vue)
|
||||
|
||||
**Breaking Changes**
|
||||
|
||||
The error returned in `const { error } = nhost.storage.upload()` is not a JavaScript `Error`, but an object of type `{ error: string; status: number; message: string}`.
|
||||
|
||||
## 1.13.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/hasura-storage-js",
|
||||
"version": "1.13.2",
|
||||
"version": "2.0.0",
|
||||
"description": "Hasura-storage client",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
@@ -59,7 +59,6 @@
|
||||
"docgen": "pnpm typedoc && docgen --config ./storage.docgen.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.2.0",
|
||||
"form-data": "^4.0.0",
|
||||
"xstate": "^4.33.5"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import { toIso88591 } from './utils'
|
||||
import fetch from 'cross-fetch'
|
||||
|
||||
import {
|
||||
ApiDeleteParams,
|
||||
@@ -8,9 +6,9 @@ import {
|
||||
ApiGetPresignedUrlParams,
|
||||
ApiGetPresignedUrlResponse,
|
||||
ApiUploadParams,
|
||||
ApiUploadResponse,
|
||||
UploadHeaders
|
||||
} from './utils'
|
||||
StorageUploadResponse
|
||||
} from './utils/types'
|
||||
import { fetchUpload } from './utils/upload'
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -18,52 +16,37 @@ import {
|
||||
*/
|
||||
export class HasuraStorageApi {
|
||||
private url: string
|
||||
private httpClient: AxiosInstance
|
||||
private accessToken?: string
|
||||
private adminSecret?: string
|
||||
|
||||
constructor({ url }: { url: string }) {
|
||||
this.url = url
|
||||
|
||||
this.httpClient = axios.create({
|
||||
baseURL: this.url
|
||||
})
|
||||
}
|
||||
|
||||
async upload(params: ApiUploadParams): Promise<ApiUploadResponse> {
|
||||
async upload(params: ApiUploadParams): Promise<StorageUploadResponse> {
|
||||
const { formData } = params
|
||||
|
||||
try {
|
||||
const res = await this.httpClient.post('/files', formData, {
|
||||
headers: {
|
||||
...this.generateUploadHeaders(params),
|
||||
...this.generateAuthHeaders(),
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
|
||||
return { fileMetadata: res.data, error: null }
|
||||
} catch (error) {
|
||||
return { fileMetadata: null, error: error as Error }
|
||||
}
|
||||
return fetchUpload(this.url, formData, {
|
||||
accessToken: this.accessToken,
|
||||
adminSecret: this.accessToken,
|
||||
bucketId: params.bucketId,
|
||||
fileId: params.id,
|
||||
name: params.name
|
||||
})
|
||||
}
|
||||
|
||||
async getPresignedUrl(params: ApiGetPresignedUrlParams): Promise<ApiGetPresignedUrlResponse> {
|
||||
try {
|
||||
const { fileId } = params
|
||||
const url = `/files/${fileId}/presignedurl`
|
||||
// TODO not implemented yet in hasura-storage
|
||||
// const { fileId, ...imageTransformationParams } = params
|
||||
// const url = appendImageTransformationParameters(
|
||||
// `/files/${fileId}/presignedurl`,
|
||||
// imageTransformationParams
|
||||
// )
|
||||
const res = await this.httpClient.get(url, {
|
||||
headers: {
|
||||
...this.generateAuthHeaders()
|
||||
}
|
||||
const response = await fetch(`${this.url}/files/${fileId}/presignedurl`, {
|
||||
method: 'GET',
|
||||
headers: this.generateAuthHeaders()
|
||||
})
|
||||
return { presignedUrl: res.data, error: null }
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text())
|
||||
}
|
||||
const presignedUrl = await response.json()
|
||||
return { presignedUrl, error: null }
|
||||
} catch (error) {
|
||||
return { presignedUrl: null, error: error as Error }
|
||||
}
|
||||
@@ -72,11 +55,13 @@ export class HasuraStorageApi {
|
||||
async delete(params: ApiDeleteParams): Promise<ApiDeleteResponse> {
|
||||
try {
|
||||
const { fileId } = params
|
||||
await this.httpClient.delete(`/files/${fileId}`, {
|
||||
headers: {
|
||||
...this.generateAuthHeaders()
|
||||
}
|
||||
const response = await fetch(`${this.url}/files/${fileId}`, {
|
||||
method: 'DELETE',
|
||||
headers: this.generateAuthHeaders()
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text())
|
||||
}
|
||||
return { error: null }
|
||||
} catch (error) {
|
||||
return { error: error as Error }
|
||||
@@ -107,29 +92,9 @@ export class HasuraStorageApi {
|
||||
return this
|
||||
}
|
||||
|
||||
private generateUploadHeaders(params: ApiUploadParams): UploadHeaders {
|
||||
const { bucketId, name, id } = params
|
||||
const uploadheaders: UploadHeaders = {}
|
||||
|
||||
if (bucketId) {
|
||||
uploadheaders['x-nhost-bucket-id'] = bucketId
|
||||
}
|
||||
if (id) {
|
||||
uploadheaders['x-nhost-file-id'] = id
|
||||
}
|
||||
if (name) {
|
||||
uploadheaders['x-nhost-file-name'] = toIso88591(name)
|
||||
}
|
||||
|
||||
return uploadheaders
|
||||
}
|
||||
|
||||
private generateAuthHeaders():
|
||||
| { Authorization: string }
|
||||
| { 'x-hasura-admin-secret': string }
|
||||
| null {
|
||||
private generateAuthHeaders(): HeadersInit | undefined {
|
||||
if (!this.adminSecret && !this.accessToken) {
|
||||
return null
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (this.adminSecret) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import FormData from 'form-data'
|
||||
|
||||
import { HasuraStorageApi } from './hasura-storage-api'
|
||||
import {
|
||||
appendImageTransformationParameters,
|
||||
@@ -86,26 +85,10 @@ export class HasuraStorageClient {
|
||||
formData = params.formData
|
||||
}
|
||||
|
||||
const { fileMetadata, error } = await this.api.upload({
|
||||
return this.api.upload({
|
||||
...params,
|
||||
formData: formData
|
||||
formData
|
||||
})
|
||||
if (error) {
|
||||
return { fileMetadata: null, error }
|
||||
}
|
||||
|
||||
if (!fileMetadata) {
|
||||
return { fileMetadata: null, error: new Error('Invalid file returned') }
|
||||
}
|
||||
|
||||
return { fileMetadata, error: null }
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use `nhost.storage.getPublicUrl()` instead.
|
||||
*/
|
||||
getUrl(params: StorageGetUrlParams): string {
|
||||
return this.getPublicUrl(params)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import axios, { AxiosError, AxiosProgressEvent, RawAxiosRequestHeaders } from 'axios'
|
||||
import FormData from 'form-data'
|
||||
import { assign, createMachine } from 'xstate'
|
||||
import { toIso88591 } from '../utils'
|
||||
|
||||
import { ErrorPayload, FileUploadConfig } from '../utils'
|
||||
import { fetchUpload } from '../utils/upload'
|
||||
|
||||
export type FileUploadContext = {
|
||||
progress: number | null
|
||||
@@ -117,76 +116,40 @@ export const createFileUploadMachine = () =>
|
||||
},
|
||||
services: {
|
||||
uploadFile: (context, event) => (callback) => {
|
||||
const headers: RawAxiosRequestHeaders = {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
const fileId = event.id || context.id
|
||||
if (fileId) {
|
||||
headers['x-nhost-file-id'] = fileId
|
||||
}
|
||||
const bucketId = event.bucketId || context.bucketId
|
||||
if (bucketId) {
|
||||
headers['x-nhost-bucket-id'] = bucketId
|
||||
}
|
||||
const file = (event.file || context.file)!
|
||||
headers['x-nhost-file-name'] = toIso88591(event.name || file.name)
|
||||
const data = new FormData()
|
||||
data.append('file', file)
|
||||
if (event.adminSecret) {
|
||||
headers['x-hasura-admin-secret'] = event.adminSecret
|
||||
}
|
||||
if (event.accessToken) {
|
||||
headers['Authorization'] = `Bearer ${event.accessToken}`
|
||||
}
|
||||
let currentLoaded = 0
|
||||
const controller = new AbortController()
|
||||
axios
|
||||
.post<{
|
||||
bucketId: string
|
||||
createdAt: string
|
||||
etag: string
|
||||
id: string
|
||||
isUploaded: true
|
||||
mimeType: string
|
||||
name: string
|
||||
size: number
|
||||
updatedAt: string
|
||||
uploadedByUserId: string
|
||||
}>(event.url + '/files', data, {
|
||||
headers,
|
||||
signal: controller.signal,
|
||||
onUploadProgress: (event: AxiosProgressEvent) => {
|
||||
const loaded = event.total
|
||||
? Math.round((event.loaded * file.size!) / event.total)
|
||||
: 0
|
||||
const additions = loaded - currentLoaded
|
||||
currentLoaded = loaded
|
||||
callback({
|
||||
type: 'UPLOAD_PROGRESS',
|
||||
progress: event.total ? Math.round((loaded * 100) / event.total) : 0,
|
||||
loaded,
|
||||
additions
|
||||
})
|
||||
}
|
||||
})
|
||||
.then(({ data: { id, bucketId } }) => {
|
||||
callback({ type: 'UPLOAD_DONE', id, bucketId })
|
||||
})
|
||||
.catch(({ response, message }: AxiosError<{ error?: { message: string } }>) => {
|
||||
callback({
|
||||
type: 'UPLOAD_ERROR',
|
||||
error: {
|
||||
status: response?.status ?? 0,
|
||||
message: response?.data?.error?.message || message,
|
||||
// TODO errors from hasura-storage are not codified
|
||||
error: response?.data?.error?.message || message
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return () => {
|
||||
controller.abort()
|
||||
}
|
||||
let currentLoaded = 0
|
||||
|
||||
fetchUpload(event.url, data, {
|
||||
fileId: event.id || context.id,
|
||||
bucketId: event.bucketId || context.bucketId,
|
||||
accessToken: event.accessToken,
|
||||
adminSecret: event.adminSecret,
|
||||
name: event.name || file.name,
|
||||
onUploadProgress: (event) => {
|
||||
const loaded = event.total ? Math.round((event.loaded * file.size!) / event.total) : 0
|
||||
const additions = loaded - currentLoaded
|
||||
currentLoaded = loaded
|
||||
callback({
|
||||
type: 'UPLOAD_PROGRESS',
|
||||
progress: event.total ? Math.round((loaded * 100) / event.total) : 0,
|
||||
loaded,
|
||||
additions
|
||||
})
|
||||
}
|
||||
}).then(({ fileMetadata, error }) => {
|
||||
if (error) {
|
||||
callback({ type: 'UPLOAD_ERROR', error })
|
||||
}
|
||||
if (fileMetadata) {
|
||||
const { id, bucketId } = fileMetadata
|
||||
callback({ type: 'UPLOAD_DONE', id, bucketId })
|
||||
}
|
||||
})
|
||||
|
||||
return () => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,3 @@ export const appendImageTransformationParameters = (
|
||||
.join('&')
|
||||
return queryParameters ? `${url}?${queryParameters}` : url
|
||||
}
|
||||
|
||||
/** Convert any string into ISO-8859-1 */
|
||||
export const toIso88591 = (fileName: string) => {
|
||||
try {
|
||||
btoa(fileName)
|
||||
return fileName
|
||||
} catch {
|
||||
return encodeURIComponent(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export type StorageUploadParams = StorageUploadFileParams | StorageUploadFormDat
|
||||
|
||||
export type StorageUploadResponse =
|
||||
| { fileMetadata: FileResponse; error: null }
|
||||
| { fileMetadata: null; error: Error }
|
||||
| { fileMetadata: null; error: ErrorPayload }
|
||||
|
||||
export interface StorageImageTransformationParams {
|
||||
/** Image width, in pixels */
|
||||
@@ -83,7 +83,7 @@ export interface StorageDeleteResponse {
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
interface FileResponse {
|
||||
export interface FileResponse {
|
||||
id: string
|
||||
name: string
|
||||
size: number
|
||||
@@ -91,6 +91,9 @@ interface FileResponse {
|
||||
etag: string
|
||||
createdAt: string
|
||||
bucketId: string
|
||||
isUploaded: true
|
||||
updatedAt: string
|
||||
uploadedByUserId: string
|
||||
}
|
||||
|
||||
export interface ApiUploadParams {
|
||||
@@ -100,10 +103,6 @@ export interface ApiUploadParams {
|
||||
bucketId?: string
|
||||
}
|
||||
|
||||
export type ApiUploadResponse =
|
||||
| { fileMetadata: FileResponse; error: null }
|
||||
| { fileMetadata: null; error: Error }
|
||||
|
||||
// TODO not implemented yet in hasura-storage
|
||||
// export interface ApiGetPresignedUrlParams extends StorageImageTransformationParams {
|
||||
export interface ApiGetPresignedUrlParams {
|
||||
@@ -122,7 +121,7 @@ export interface ApiDeleteResponse {
|
||||
error: Error | null
|
||||
}
|
||||
|
||||
export interface UploadHeaders {
|
||||
export type UploadHeaders = HeadersInit & {
|
||||
'x-nhost-bucket-id'?: string
|
||||
'x-nhost-file-id'?: string
|
||||
'x-nhost-file-name'?: string
|
||||
|
||||
118
packages/hasura-storage-js/src/utils/upload.ts
Normal file
118
packages/hasura-storage-js/src/utils/upload.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import fetch from 'cross-fetch'
|
||||
import FormData from 'form-data'
|
||||
|
||||
import { ErrorPayload, StorageUploadResponse } from './types'
|
||||
|
||||
/** Convert any string into ISO-8859-1 */
|
||||
export const toIso88591 = (fileName: string) => {
|
||||
try {
|
||||
btoa(fileName)
|
||||
return fileName
|
||||
} catch {
|
||||
return encodeURIComponent(fileName)
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchUpload = async (
|
||||
backendUrl: string,
|
||||
data: FormData,
|
||||
{
|
||||
accessToken,
|
||||
name,
|
||||
fileId,
|
||||
bucketId,
|
||||
adminSecret,
|
||||
onUploadProgress
|
||||
}: {
|
||||
accessToken?: string
|
||||
name?: string
|
||||
fileId?: string
|
||||
bucketId?: string
|
||||
adminSecret?: string
|
||||
onUploadProgress?: (event: { total: number; loaded: number }) => void
|
||||
} = {}
|
||||
): Promise<StorageUploadResponse> => {
|
||||
const headers: HeadersInit = {}
|
||||
if (fileId) {
|
||||
headers['x-nhost-file-id'] = fileId
|
||||
}
|
||||
if (bucketId) {
|
||||
headers['x-nhost-bucket-id'] = bucketId
|
||||
}
|
||||
if (name) {
|
||||
headers['x-nhost-file-name'] = toIso88591(name)
|
||||
}
|
||||
if (adminSecret) {
|
||||
headers['x-hasura-admin-secret'] = adminSecret
|
||||
}
|
||||
if (accessToken) {
|
||||
headers['Authorization'] = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const url = `${backendUrl}/files`
|
||||
if (typeof XMLHttpRequest === 'undefined') {
|
||||
// * Non-browser environment: XMLHttpRequest is not available
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: data as any // * https://github.com/form-data/form-data/issues/513
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const error: ErrorPayload = {
|
||||
status: response.status,
|
||||
message: await response.text(),
|
||||
// * errors from hasura-storage are not codified
|
||||
error: response.statusText
|
||||
}
|
||||
return { error, fileMetadata: null }
|
||||
}
|
||||
const fileMetadata = await response.json()
|
||||
return { fileMetadata, error: null }
|
||||
} catch (e) {
|
||||
const error: ErrorPayload = {
|
||||
status: 0,
|
||||
message: (e as Error).message,
|
||||
error: (e as Error).message
|
||||
}
|
||||
return { error, fileMetadata: null }
|
||||
}
|
||||
}
|
||||
// * Browser environment: XMLHttpRequest is available
|
||||
return new Promise((resolve) => {
|
||||
console.log('NOOOOOOO')
|
||||
let xhr = new XMLHttpRequest()
|
||||
xhr.responseType = 'json'
|
||||
|
||||
xhr.onload = () => {
|
||||
if (xhr.status < 200 && xhr.status >= 300) {
|
||||
return resolve({
|
||||
fileMetadata: null,
|
||||
error: { error: xhr.statusText, message: xhr.statusText, status: xhr.status }
|
||||
})
|
||||
}
|
||||
return resolve({ fileMetadata: xhr.response, error: null })
|
||||
}
|
||||
|
||||
xhr.onerror = () => {
|
||||
// only triggers if the request couldn't be made at all e.g. network error
|
||||
return resolve({
|
||||
fileMetadata: null,
|
||||
error: { error: xhr.statusText, message: xhr.statusText, status: xhr.status }
|
||||
})
|
||||
}
|
||||
|
||||
if (onUploadProgress) {
|
||||
xhr.upload.addEventListener('progress', onUploadProgress, false)
|
||||
}
|
||||
|
||||
xhr.open('POST', url, true)
|
||||
|
||||
Object.entries(headers).forEach(([key, value]) => {
|
||||
xhr.setRequestHeader(key, value)
|
||||
})
|
||||
|
||||
xhr.send(data as any) // * https://github.com/form-data/form-data/issues/513
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { toIso88591 } from '../src/utils'
|
||||
import { toIso88591 } from '../src/utils/upload'
|
||||
|
||||
describe('test file names', () => {
|
||||
it('should be able to use ISO 8859-1 strings', async () => {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user