Compare commits

...

8 Commits

79 changed files with 1819 additions and 11000 deletions

View File

@@ -49,4 +49,4 @@ This repository is a monorepo that contains multiple packages and applications.
- `tools/codegen` - Internal code generation tool to build the SDK
- `tools/mintlify-openapi` - Internal tool to generate reference documentation for Mintlify from an OpenAPI spec.
For details about those projects and how to contribure, please refer to their respective `README.md` and `CONTRIBUTING.md` files.
For details about those projects and how to contribute, please refer to their respective `README.md` and `CONTRIBUTING.md` files.

View File

@@ -107,6 +107,7 @@ Nhost is frontend agnostic, which means Nhost works with all frontend frameworks
# Resources
- Start developing locally with the [Nhost CLI](https://docs.nhost.io/platform/cli/local-development)
## Nhost Clients
- [JavaScript/TypeScript](https://docs.nhost.io/reference/javascript/nhost-js/main)
@@ -137,7 +138,7 @@ Here are some ways of contributing to making Nhost better:
- **[Try out Nhost](https://docs.nhost.io)**, and think of ways to make the service better. Let us know here on GitHub.
- Join our [Discord](https://discord.com/invite/9V7Qb2U) and connect with other members to share and learn from.
- Send a pull request to any of our [open source repositories](https://github.com/nhost) on Github. Check our [contribution guide](https://github.com/nhost/nhost/blob/main/CONTRIBUTING.md) and our [developers guide](https://github.com/nhost/nhost/blob/main/DEVELOPERS.md) for more details about how to contribute. We're looking forward to your contribution!
- Send a pull request to any of our [open source repositories](https://github.com/nhost) on Github. Check out our [contribution guide](https://github.com/nhost/nhost/blob/main/CONTRIBUTING.md) for more details about how to contribute. We're looking forward to your contribution!
### Contributors

View File

@@ -3,7 +3,6 @@
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
"moderate": true,
"allowlist": [
"GHSA-9965-vmph-33xx", // https://github.com/advisories/GHSA-9965-vmph-33xx Update package once have a fix
"GHSA-7mvr-c777-76hp" // https://github.com/advisories/GHSA-7mvr-c777-76hp Update package once Nix side is also updated
]
}
}

View File

@@ -126,7 +126,7 @@
"timezones-list": "^3.1.0",
"utility-types": "^3.11.0",
"uuid": "^9.0.1",
"validator": "^13.11.0",
"validator": "^13.15.20",
"yup": "^1.4.0",
"yup-password": "^0.2.2",
"zod": "^3.23.8"
@@ -232,7 +232,9 @@
}
},
"overrides": {
"esbuild@<=0.24.2": ">=0.25.0"
"esbuild@<=0.24.2": ">=0.25.0",
"js-yaml@<=4.1.0": ">=4.1.1",
"glob@>=10.3.7 <=11.0.3": ">=11.1.0"
}
}
}

128
dashboard/pnpm-lock.yaml generated
View File

@@ -6,6 +6,8 @@ settings:
overrides:
esbuild@<=0.24.2: '>=0.25.0'
js-yaml@<=4.1.0: '>=4.1.1'
glob@>=10.3.7 <=11.0.3: '>=11.1.0'
packageExtensionsChecksum: sha256-gRFeykwiwMfEE6etcYx6N48XwVeKzxbqNveL7KTQgSQ=
@@ -320,8 +322,8 @@ importers:
specifier: ^9.0.1
version: 9.0.1
validator:
specifier: ^13.11.0
version: 13.12.0
specifier: ^13.15.20
version: 13.15.23
yup:
specifier: ^1.4.0
version: 1.5.0
@@ -2245,6 +2247,14 @@ packages:
'@types/node':
optional: true
'@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22}
'@isaacs/brace-expansion@5.0.0':
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
engines: {node: 20 || >=22}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -2618,10 +2628,6 @@ packages:
'@orval/zod@7.11.2':
resolution: {integrity: sha512-4MzTg5Wms8/LlM3CbYu80dvCbP88bVlQjnYsBdFXuEv0K2GYkBCAhVOrmXCVrPXE89neV6ABkvWQeuKZQpkdxQ==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@playwright/test@1.54.1':
resolution: {integrity: sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==}
engines: {node: '>=18'}
@@ -5649,8 +5655,8 @@ packages:
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
engines: {node: '>= 0.4'}
foreground-child@3.1.1:
resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
foreground-child@3.3.1:
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
engines: {node: '>=14'}
form-data@4.0.4:
@@ -5779,8 +5785,9 @@ packages:
glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
glob@10.4.5:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
glob@12.0.0:
resolution: {integrity: sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==}
engines: {node: 20 || >=22}
hasBin: true
glob@7.1.7:
@@ -6288,9 +6295,9 @@ packages:
iterator.prototype@1.1.2:
resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==}
jackspeak@3.2.3:
resolution: {integrity: sha512-htOzIMPbpLid/Gq9/zaz9SfExABxqRe1sSCdxntlO/aMD6u0issZQiY25n2GKQUtJ02j7z5sfptlAOMpWWOmvw==}
engines: {node: '>=14'}
jackspeak@4.1.1:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22}
jest-diff@29.7.0:
resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==}
@@ -6337,8 +6344,8 @@ packages:
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsdom@22.1.0:
@@ -6555,9 +6562,9 @@ packages:
lowlight@3.1.0:
resolution: {integrity: sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ==}
lru-cache@10.2.2:
resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==}
engines: {node: 14 || >=16.14}
lru-cache@11.2.2:
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
engines: {node: 20 || >=22}
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
@@ -6798,6 +6805,10 @@ packages:
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
hasBin: true
minimatch@10.1.1:
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
engines: {node: 20 || >=22}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -6809,10 +6820,6 @@ packages:
resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
engines: {node: '>=16 || 14 >=14.17'}
minimatch@9.0.4:
resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==}
engines: {node: '>=16 || 14 >=14.17'}
minimatch@9.0.5:
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
engines: {node: '>=16 || 14 >=14.17'}
@@ -7181,9 +7188,9 @@ packages:
resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==}
engines: {node: '>=0.10.0'}
path-scurry@1.11.1:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
path-scurry@2.0.1:
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
engines: {node: 20 || >=22}
path-to-regexp@6.3.0:
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
@@ -8594,8 +8601,8 @@ packages:
v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
validator@13.12.0:
resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==}
validator@13.15.23:
resolution: {integrity: sha512-4yoz1kEWqUjzi5zsPbAS/903QXSYp0UOtHsPpp7p9rHAw/W+dkInskAE386Fat3oKRROwO98d9ZB0G4cObgUyw==}
engines: {node: '>= 0.10'}
vfile-message@4.0.2:
@@ -8936,7 +8943,7 @@ snapshots:
dependencies:
'@jsdevtools/ono': 7.1.3
'@types/json-schema': 7.0.15
js-yaml: 4.1.0
js-yaml: 4.1.1
'@apidevtools/openapi-schemas@2.1.0': {}
@@ -10335,7 +10342,7 @@ snapshots:
globals: 13.24.0
ignore: 5.3.2
import-fresh: 3.3.0
js-yaml: 4.1.0
js-yaml: 4.1.1
minimatch: 3.1.2
strip-json-comments: 3.1.1
transitivePeerDependencies:
@@ -11019,7 +11026,7 @@ snapshots:
loglevel: 1.9.2
loglevel-plugin-prefix: 0.8.4
minimatch: 6.2.0
validator: 13.12.0
validator: 13.15.23
transitivePeerDependencies:
- encoding
@@ -11152,11 +11159,17 @@ snapshots:
optionalDependencies:
'@types/node': 20.14.8
'@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0':
dependencies:
'@isaacs/balanced-match': 4.0.1
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
string-width-cjs: string-width@4.2.3
strip-ansi: 7.1.0
strip-ansi: 7.1.2
strip-ansi-cjs: strip-ansi@6.0.1
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
@@ -11604,9 +11617,6 @@ snapshots:
- openapi-types
- supports-color
'@pkgjs/parseargs@0.11.0':
optional: true
'@playwright/test@1.54.1':
dependencies:
playwright: 1.54.1
@@ -14198,7 +14208,7 @@ snapshots:
cosmiconfig@8.3.6(typescript@5.8.3):
dependencies:
import-fresh: 3.3.1
js-yaml: 4.1.0
js-yaml: 4.1.1
parse-json: 5.2.0
path-type: 4.0.0
optionalDependencies:
@@ -14208,7 +14218,7 @@ snapshots:
dependencies:
env-paths: 2.2.1
import-fresh: 3.3.1
js-yaml: 4.1.0
js-yaml: 4.1.1
parse-json: 5.2.0
optionalDependencies:
typescript: 5.8.3
@@ -15062,7 +15072,7 @@ snapshots:
imurmurhash: 0.1.4
is-glob: 4.0.3
is-path-inside: 3.0.3
js-yaml: 4.1.0
js-yaml: 4.1.1
json-stable-stringify-without-jsonify: 1.0.1
levn: 0.4.1
lodash.merge: 4.6.2
@@ -15241,7 +15251,7 @@ snapshots:
dependencies:
is-callable: 1.2.7
foreground-child@3.1.1:
foreground-child@3.3.1:
dependencies:
cross-spawn: 7.0.6
signal-exit: 4.1.0
@@ -15382,14 +15392,14 @@ snapshots:
glob-to-regexp@0.4.1: {}
glob@10.4.5:
glob@12.0.0:
dependencies:
foreground-child: 3.1.1
jackspeak: 3.2.3
minimatch: 9.0.4
foreground-child: 3.3.1
jackspeak: 4.1.1
minimatch: 10.1.1
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
path-scurry: 2.0.1
glob@7.1.7:
dependencies:
@@ -15912,11 +15922,9 @@ snapshots:
reflect.getprototypeof: 1.0.8
set-function-name: 2.0.2
jackspeak@3.2.3:
jackspeak@4.1.1:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jest-diff@29.7.0:
dependencies:
@@ -15973,7 +15981,7 @@ snapshots:
js-tokens@9.0.1: {}
js-yaml@4.1.0:
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
@@ -16210,7 +16218,7 @@ snapshots:
devlop: 1.1.0
highlight.js: 11.9.0
lru-cache@10.2.2: {}
lru-cache@11.2.2: {}
lru-cache@5.1.1:
dependencies:
@@ -16640,6 +16648,10 @@ snapshots:
mini-svg-data-uri@1.4.4: {}
minimatch@10.1.1:
dependencies:
'@isaacs/brace-expansion': 5.0.0
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
@@ -16652,10 +16664,6 @@ snapshots:
dependencies:
brace-expansion: 2.0.2
minimatch@9.0.4:
dependencies:
brace-expansion: 2.0.2
minimatch@9.0.5:
dependencies:
brace-expansion: 2.0.2
@@ -17094,9 +17102,9 @@ snapshots:
dependencies:
path-root-regex: 0.1.2
path-scurry@1.11.1:
path-scurry@2.0.1:
dependencies:
lru-cache: 10.2.2
lru-cache: 11.2.2
minipass: 7.1.2
path-to-regexp@6.3.0: {}
@@ -17942,7 +17950,7 @@ snapshots:
dependencies:
eastasianwidth: 0.2.0
emoji-regex: 9.2.2
strip-ansi: 7.1.0
strip-ansi: 7.1.2
string-width@7.2.0:
dependencies:
@@ -18061,7 +18069,7 @@ snapshots:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
commander: 4.1.1
glob: 10.4.5
glob: 12.0.0
lines-and-columns: 1.2.4
mz: 2.7.0
pirates: 4.0.6
@@ -18163,7 +18171,7 @@ snapshots:
test-exclude@7.0.1:
dependencies:
'@istanbuljs/schema': 0.1.3
glob: 10.4.5
glob: 12.0.0
minimatch: 9.0.5
text-table@0.2.0: {}
@@ -18535,7 +18543,7 @@ snapshots:
v8-compile-cache-lib@3.0.1: {}
validator@13.12.0: {}
validator@13.15.23: {}
vfile-message@4.0.2:
dependencies:
@@ -18789,9 +18797,9 @@ snapshots:
wrap-ansi@8.1.0:
dependencies:
ansi-styles: 6.2.1
ansi-styles: 6.2.3
string-width: 5.1.2
strip-ansi: 7.1.0
strip-ansi: 7.1.2
wrap-ansi@9.0.0:
dependencies:

View File

@@ -0,0 +1,80 @@
import { Button } from '@/components/ui/v3/button';
import { Input } from '@/components/ui/v3/input';
import {
useDataGridFilter,
type DataGridFilterOperator,
} from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
import { cn, isNotEmptyValue } from '@/lib/utils';
import { X } from 'lucide-react';
import DataGridFilterColumn from './DataGridFilterColumn';
import DataGridFilterOperators from './DataGridFilterOperators';
type FilterProps = {
column: string;
op: DataGridFilterOperator;
value: string;
index: number;
columns: Array<{ id: string; dataType: string }>;
error?: string;
};
function DataGridFilter({
column,
op,
value,
index,
columns,
error,
}: FilterProps) {
const { setColumn, setOp, setValue, removeFilter } = useDataGridFilter();
function handleOpChange(newOp: DataGridFilterOperator) {
setOp(index, newOp);
if (
newOp === '$like' ||
newOp === '$ilike' ||
newOp === '$nlike' ||
newOp === '$nilike'
) {
setValue(index, '%%');
} else if (newOp === '$in' || newOp === '$nin') {
setValue(index, '[]');
}
}
return (
<div className="flex gap-2">
<DataGridFilterColumn
value={column}
onChange={(newColumn) => setColumn(index, newColumn)}
columns={columns}
/>
<DataGridFilterOperators value={op} onChange={handleOpChange} />
<div className="flex-1">
<Input
className={cn('h-8 p-2', {
'border-destructive': isNotEmptyValue(error),
})}
placeholder="Enter a value"
value={value}
onChange={(event) => setValue(index, event.target.value)}
/>
<span
className={`inline-flex h-[0.875rem] text-xs- text-destructive ${isNotEmptyValue(error) ? 'visible' : 'invisible'}`}
>
{error}
</span>
</div>
<Button
variant="outline"
size="icon"
className="flex-i h-8 w-8"
onClick={() => removeFilter(index)}
>
<X width={12} height={12} />
</Button>
</div>
);
}
export default DataGridFilter;

View File

@@ -0,0 +1,42 @@
import { Badge } from '@/components/ui/v3/badge';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from '@/components/ui/v3/select';
type DataFilterColumnProps = {
value: string;
onChange: (newValue: string) => void;
columns: Array<{ id: string; dataType: string }>;
};
function DataGrdiFitlerColumn({
value,
onChange,
columns,
}: DataFilterColumnProps) {
return (
<Select value={value} onValueChange={onChange}>
<SelectTrigger className="mp-2 h-8 max-w-[35%]">
<span className="!inline-block w-4/5 justify-start overflow-ellipsis text-left">
{value}
</span>
</SelectTrigger>
<SelectContent>
{columns.map((column) => (
<SelectItem key={column.id} value={column.id}>
{column.id}{' '}
<Badge className="rounded-sm+ bg-secondary p-1 text-[0.75rem] font-normal leading-[0.75]">
{/* TODO: Fix type */}
{(column as any).dataType}
</Badge>
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export default DataGrdiFitlerColumn;

View File

@@ -0,0 +1,50 @@
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from '@/components/ui/v3/select';
import type { DataGridFilterOperator } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
const OPERATORS = [
{ op: '$eq', label: '[$eq] equals' },
{ op: '$ne', label: '[$ne] not equals' },
{ op: '$in', label: '[$in] in' },
{ op: '$nin', label: '[$nin] not in' },
{ op: '$gt', label: '[$gt] >' },
{ op: '$lt', label: '[$lt] <' },
{ op: '$gte', label: '[$gte] >=' },
{ op: '$lte', label: '[$lte] <=' },
{ op: '$like', label: '[$like] like' },
{ op: '$nlike', label: '[$nlike] not like' },
{ op: '$ilike', label: '[$ilike] like (case-insensitive)' },
{ op: '$nilike', label: '[$nilike] not like (case-insensitive)' },
{ op: '$similar', label: '[$similar] similar' },
{ op: '$nsimilar', label: '[$nsimilar] not similar' },
{ op: '$regex', label: '[$regex] ~' },
{ op: '$iregex', label: '[$iregex] ~*' },
{ op: '$nregex', label: '[$nregex] !~' },
{ op: '$niregex', label: '[$niregex] !~*' },
];
type DataFilterProps = {
value: DataGridFilterOperator;
onChange: (newOp: DataGridFilterOperator) => void;
};
function DataGridOperators({ value, onChange }: DataFilterProps) {
return (
<Select value={value} onValueChange={onChange}>
<SelectTrigger className="h-8 w-[6rem] p-2">{value}</SelectTrigger>
<SelectContent>
{OPERATORS.map(({ op, label }) => (
<SelectItem key={op} value={op}>
<span>[{op}]</span> <span className="text-secondary">{label}</span>
</SelectItem>
))}
</SelectContent>
</Select>
);
}
export default DataGridOperators;

View File

@@ -0,0 +1,34 @@
import { Button, type ButtonProps } from '@/components/ui/v3/button';
import { useDataGridFilter } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
import { cn } from '@/lib/utils';
import { Funnel } from 'lucide-react';
import { type ForwardedRef, forwardRef } from 'react';
function DataBrowserCustomizerTrigger(
props: ButtonProps,
ref: ForwardedRef<HTMLButtonElement>,
) {
const { appliedFilters } = useDataGridFilter();
const numberOfAppliedFilters = appliedFilters.length;
const { className, ...buttonProps } = props;
return (
<Button
ref={ref}
variant="outline"
size="icon"
className={cn('relative', className)}
{...buttonProps}
>
<Funnel />
{numberOfAppliedFilters > 0 && (
<span className="absolute bottom-[6px] right-[6px] w-[0.725rem] rounded-full bg-primary-text p-0 text-[0.725rem] leading-none text-paper">
{numberOfAppliedFilters}
</span>
)}
</Button>
);
}
export default forwardRef(DataBrowserCustomizerTrigger);

View File

@@ -0,0 +1,104 @@
import { Button } from '@/components/ui/v3/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/v3/popover';
import {
type DataGridFilter as Filter,
useDataGridFilter,
} from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
import { isEmptyValue, isNotEmptyValue } from '@/lib/utils';
import { useState } from 'react';
import { v4 as uuidV4 } from 'uuid';
import DataGridFilter from './DataGridFilter';
import DataGridFilterTrigger from './DataGridFilterTrigger';
function hasErrors(filters: Filter[]) {
return filters.reduce((errors, { op, value, column }, index) => {
if (isEmptyValue(value)) {
return { ...errors, [`${column}.${index}`]: 'Empty filter' };
}
if (['$in', '$nin'].includes(op)) {
try {
JSON.parse(value);
} catch {
return {
...errors,
[`${column}.${index}`]: 'Invalid format. ["item1","item 2"]',
};
}
}
return errors;
}, {});
}
function DataGridFilters() {
const { filters, addFilter, appliedFilters, setFilters, setAppliedFilters } =
useDataGridFilter();
const { columns } = useDataGridConfig();
const [errors, setErrors] = useState({});
function resetFilters() {
setFilters(appliedFilters);
}
function handleApplyFilter() {
const filterErrors = hasErrors(filters);
setErrors(filterErrors);
if (isEmptyValue(filterErrors)) {
setAppliedFilters(filters);
}
}
function handleOpenChange(newOpenState: boolean) {
if (!newOpenState) {
resetFilters();
}
}
function handleAddFilter() {
addFilter({ column: columns[0].id, op: '$eq', value: '', id: uuidV4() });
}
return (
<Popover onOpenChange={handleOpenChange}>
<PopoverTrigger asChild>
<DataGridFilterTrigger />
</PopoverTrigger>
<PopoverContent align="end" className="flex w-[40rem] flex-col gap-6 p-0">
<div className="flex w-full flex-col gap-0 px-3 pb-0 pt-6">
{isNotEmptyValue(filters) &&
filters.map((filter, index) => (
<DataGridFilter
{...filter}
key={filter.id}
index={index}
columns={columns as any}
error={errors[`${filter.column}.${index}`]}
/>
))}
{isEmptyValue(filters) && (
<p>
<strong>No filters applied to this table</strong>
<br />
Add a filter below to filter the table
</p>
)}
</div>
<div className="flex items-center justify-between border-t-1 border-t-[#e2e8f0] p-3 dark:border-t-[#2f363d]">
<Button variant="outline" size="sm" onClick={handleAddFilter}>
Add filter
</Button>
<Button variant="outline" size="sm" onClick={handleApplyFilter}>
Apply filter
</Button>
</div>
</PopoverContent>
</Popover>
);
}
export default DataGridFilters;

View File

@@ -0,0 +1 @@
export { default as DataGridFilters } from './DataGridFilters';

View File

@@ -9,7 +9,6 @@ export default function useTablePath() {
const {
query: { dataSourceSlug, schemaSlug, tableSlug },
} = useRouter();
if (!dataSourceSlug || !schemaSlug || !tableSlug) {
return '';
}

View File

@@ -3,6 +3,7 @@ import { FormActivityIndicator } from '@/components/form/FormActivityIndicator';
import { InlineCode } from '@/components/ui/v3/inline-code';
import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
import { DataBrowserEmptyState } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserEmptyState';
import { useDataGridFilter } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
import { DataBrowserGridControls } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGridControls';
import { useTableQuery } from '@/features/orgs/projects/database/dataGrid/hooks/useTableQuery';
import type { UpdateRecordVariables } from '@/features/orgs/projects/database/dataGrid/hooks/useUpdateRecordMutation';
@@ -26,6 +27,7 @@ import { DataGridNumericCell } from '@/features/orgs/projects/storage/dataGrid/c
import { DataGridTextCell } from '@/features/orgs/projects/storage/dataGrid/components/DataGridTextCell';
import { isNotEmptyValue } from '@/lib/utils';
import { useTableRows } from '@/features/orgs/projects/database/dataGrid/hooks/useTableRows';
import { useQueryClient } from '@tanstack/react-query';
import { KeyRound } from 'lucide-react';
import dynamic from 'next/dynamic';
@@ -157,16 +159,45 @@ export default function DataBrowserGrid({
const [currentOffset, setCurrentOffset] = useState<number>(
parseInt(page as string, 10) - 1 || 0,
);
const { appliedFilters } = useDataGridFilter();
const { mutateAsync: updateRow } = useUpdateRecordWithToastMutation();
const sortByString = isNotEmptyValue(sortBy?.[0])
? `${sortBy[0].id}.${sortBy[0].desc}`
: 'default-order';
const filterString = isNotEmptyValue(appliedFilters)
? appliedFilters
.map((filter) => `${filter.column}-${filter.op}-${filter.value}`)
.join('')
: 'no-filter';
const { data, status, error, refetch } = useTableQuery(
[currentTablePath, currentOffset, sortByString],
const {
data,
status,
error,
refetch,
isFetching: isTableDataFetching,
} = useTableQuery([currentTablePath], {
limit,
offset: currentOffset * limit,
orderBy:
sortBy?.map(({ id, desc }) => ({
columnName: id,
mode: desc ? 'DESC' : 'ASC',
})) || [],
filters: appliedFilters,
});
const { columns, metadata } = data || {
columns: [] as NormalizedQueryDataRow[],
};
const columnNames = columns?.map((column) => column.column_name);
const { data: tableRowsData, isFetching: isTableRowsFetching } = useTableRows(
[currentTablePath, currentOffset, sortByString, filterString],
{
columnNames,
limit,
offset: currentOffset * limit,
orderBy:
@@ -174,15 +205,16 @@ export default function DataBrowserGrid({
columnName: id,
mode: desc ? 'DESC' : 'ASC',
})) || [],
filters: appliedFilters,
queryOptions: {
enabled: !isTableDataFetching,
},
},
);
const { columns, rows, numberOfRows, metadata } = data || {
columns: [] as NormalizedQueryDataRow[],
rows: [] as NormalizedQueryDataRow[],
numberOfRows: 0,
};
const rows = tableRowsData?.rows || ([] as NormalizedQueryDataRow[]);
const numberOfRows = tableRowsData?.numberOfRows || 0;
const tableRowsError = tableRowsData?.error;
useEffect(() => {
if (
currentTablePath &&
@@ -262,8 +294,6 @@ export default function DataBrowserGrid({
[columns, currentTablePath, queryClient, updateRow],
);
const memoizedData = useMemo(() => rows, [rows]);
async function handleInsertRowClick() {
openDrawer({
title: 'Insert a New Row',
@@ -324,11 +354,17 @@ export default function DataBrowserGrid({
<DataGrid
ref={dataGridRef}
columns={memoizedColumns}
data={memoizedData}
data={rows}
allowSelection
allowResize
allowSort
emptyStateMessage="No rows found."
emptyStateMessage={
tableRowsError ? (
<span className="text-destructive">Error: {tableRowsError}</span>
) : (
'No rows found.'
)
}
loading={status === 'loading'}
sortBy={sortBy}
className="pb-17 sm:pb-0"
@@ -352,6 +388,7 @@ export default function DataBrowserGrid({
refetchData={refetch}
/>
}
isFetching={!!isTableRowsFetching}
{...props}
/>
);

View File

@@ -0,0 +1,168 @@
import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
import PersistenDataGrdiFilterStorage from '@/features/orgs/projects/database/dataGrid/utils/PersistentDataGridFilterStorage';
import {
createContext,
useContext,
useEffect,
useMemo,
useState,
type PropsWithChildren,
} from 'react';
function updateFilterInArray(
filters: DataGridFilter[],
index: number,
newValue: DataGridFilter,
) {
return [...filters.slice(0, index), newValue, ...filters.slice(index + 1)];
}
function updateFilter(
oldFilters: DataGridFilter[],
index: number,
filterKey: keyof DataGridFilter,
newValue: string | DataGridFilterOperator,
) {
const filter = oldFilters[index];
const filterToUpdate = {
...filter,
[filterKey]: newValue,
};
return updateFilterInArray(oldFilters, index, filterToUpdate);
}
export type DataGridFilterOperator =
| '$eq'
| '$ne'
| '$in'
| '$nin'
| '$gt'
| '$lt'
| '$gte'
| '$lte'
| '$like'
| '$nlike'
| '$ilike'
| '$nilike'
| '$similar'
| '$nsimilar'
| '$regex'
| '$iregex'
| '$nregex'
| '$niregex';
export type DataGridFilter = {
column: string;
op: DataGridFilterOperator;
value: string;
id: string;
};
type DataGridFilterContextProps = {
appliedFilters: DataGridFilter[];
setAppliedFilters: (filters: DataGridFilter[]) => void;
filters: DataGridFilter[];
setFilters: (filters: DataGridFilter[]) => void;
addFilter: (newFilter: DataGridFilter) => void;
removeFilter: (index: number) => void;
setValue: (index: number, newValue: string) => void;
setColumn: (index: number, newColumn: string) => void;
setOp: (index: number, newOp: DataGridFilterOperator) => void;
};
const DataGridFilterContext = createContext<DataGridFilterContextProps>({
appliedFilters: [] as DataGridFilter[],
setAppliedFilters: () => {},
filters: [] as DataGridFilter[],
setFilters: () => {},
addFilter: () => {},
removeFilter: () => {},
setValue: () => {},
setColumn: () => {},
setOp: () => {},
});
function DataGridFilterProvider({ children }: PropsWithChildren) {
const tablePath = useTablePath();
const [appliedFilters, _setAppliedFilters] = useState<DataGridFilter[]>(() =>
PersistenDataGrdiFilterStorage.getDataGridFilters(tablePath),
);
const [filters, setFilters] = useState<DataGridFilter[]>(() =>
PersistenDataGrdiFilterStorage.getDataGridFilters(tablePath),
);
// const [loadedFiltersTablePath, setLoadedFiltersTablePath] = useState(
// () => tablePath,
// );
// const test = useRef<string | null>(null);
function addFilter(newFilter: DataGridFilter) {
setFilters((oldFilters) => oldFilters.concat(newFilter));
}
function setAppliedFilters(newFilters: DataGridFilter[]) {
_setAppliedFilters(newFilters);
PersistenDataGrdiFilterStorage.saveDataGridFilters(tablePath, newFilters);
}
useEffect(() => {
const filtersForTheTable =
PersistenDataGrdiFilterStorage.getDataGridFilters(tablePath);
setFilters(filtersForTheTable);
_setAppliedFilters(filtersForTheTable);
}, [tablePath]);
const contextValue: DataGridFilterContextProps = useMemo(
() => ({
filters,
setFilters,
appliedFilters,
setAppliedFilters,
addFilter,
removeFilter(index: number) {
setFilters((oldFilters) => {
const newFilters = oldFilters.filter((_, i) => index !== i);
PersistenDataGrdiFilterStorage.saveDataGridFilters(
tablePath,
newFilters,
);
return newFilters;
});
},
setColumn(index: number, newColumnValue: string) {
setFilters((oldFilters) =>
updateFilter(oldFilters, index, 'column', newColumnValue),
);
},
setValue(index: number, newValue: string) {
setFilters((oldFilters) =>
updateFilter(oldFilters, index, 'value', newValue),
);
},
setOp(index: number, newOp: DataGridFilterOperator) {
setFilters((oldFilters) =>
updateFilter(oldFilters, index, 'op', newOp),
);
},
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[appliedFilters, filters],
);
return (
<DataGridFilterContext.Provider value={contextValue}>
{children}
</DataGridFilterContext.Provider>
);
}
export default DataGridFilterProvider;
export function useDataGridFilter() {
const context = useContext(DataGridFilterContext);
return context;
}

View File

@@ -0,0 +1,3 @@
export { default as DataGridFilterProvider } from './DataGridFilterProvider';
export * from './DataGridFilterProvider';

View File

@@ -2,6 +2,7 @@ import { useDialog } from '@/components/common/DialogProvider';
import { Badge } from '@/components/ui/v3/badge';
import { ButtonWithLoading as Button } from '@/components/ui/v3/button';
import { DataGridCustomizerControls } from '@/features/orgs/projects/common/components/DataGridCustomizerControls';
import { DataGridFilters } from '@/features/orgs/projects/common/components/DataGridFilters';
import { useDeleteRecordMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useDeleteRecordMutation';
import type { DataBrowserGridColumn } from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
import { useDataGridConfig } from '@/features/orgs/projects/storage/dataGrid/components/DataGridConfigProvider';
@@ -156,6 +157,7 @@ export default function DataBrowserGridControls({
{...restPaginationProps}
/>
)}
<DataGridFilters />
<DataGridCustomizerControls />
<Button onClick={onInsertRowClick} size="sm">
<Plus className="h-4 w-4" /> Insert row

View File

@@ -1,3 +1,4 @@
import type { DataGridFilter } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
import type {
ForeignKeyRelation,
MutationOrQueryBaseOptions,
@@ -9,7 +10,6 @@ import type {
import { extractForeignKeyRelation } from '@/features/orgs/projects/database/dataGrid/utils/extractForeignKeyRelation';
import { getPreparedReadOnlyHasuraQuery } from '@/features/orgs/projects/database/dataGrid/utils/hasuraQueryHelpers';
import { POSTGRESQL_ERROR_CODES } from '@/features/orgs/projects/database/dataGrid/utils/postgresqlConstants';
import { formatWithArray } from 'node-pg-format';
export interface FetchTableOptions extends MutationOrQueryBaseOptions {
/**
@@ -30,6 +30,12 @@ export interface FetchTableOptions extends MutationOrQueryBaseOptions {
* Determines whether the query should fetch the rows or not.
*/
preventRowFetching?: boolean;
/**
* Filtering configuration.
*
* @default []
*/
filters?: DataGridFilter[];
}
export interface FetchTableReturnType {
@@ -37,18 +43,10 @@ export interface FetchTableReturnType {
* List of columns in the table.
*/
columns: NormalizedQueryDataRow[];
/**
* List of rows in the table.
*/
rows: NormalizedQueryDataRow[];
/**
* Foreign key relations in the table.
*/
foreignKeyRelations: ForeignKeyRelation[];
/**
* Total number of rows in the table.
*/
numberOfRows: number;
/**
* Response metadata that usually contains information about the schema and
* the table for which the query was run.
@@ -68,43 +66,7 @@ export default async function fetchTable({
table,
appUrl,
adminSecret,
limit,
offset,
orderBy,
preventRowFetching,
}: FetchTableOptions): Promise<FetchTableReturnType> {
let limitAndOffsetClause = '';
if (preventRowFetching) {
limitAndOffsetClause = `LIMIT 0`;
} else if (limit && offset) {
limitAndOffsetClause = `LIMIT ${limit} OFFSET ${offset}`;
} else if (limit) {
limitAndOffsetClause = `LIMIT ${limit}`;
}
let orderByClause = 'ORDER BY 1';
if (orderBy && orderBy.length > 0) {
// Note: This part will be added to the SQL template
const pgFormatTemplate = orderBy.map(() => '%I %s').join(' ');
// Note: We are flattening object values so that we can pass them to the
// formatter function as arguments
const flattenedOrderByValues = orderBy.reduce<OrderBy[]>(
(values, currentOrderBy) => {
const currentValues = Object.values(currentOrderBy) as OrderBy[];
return [...values, ...currentValues];
},
[],
);
orderByClause = formatWithArray(
`ORDER BY ${pgFormatTemplate}`,
flattenedOrderByValues,
);
}
const response = await fetch(`${appUrl}/v2/query`, {
method: 'POST',
headers: {
@@ -153,14 +115,6 @@ export default async function fetchTable({
schema,
table,
),
getPreparedReadOnlyHasuraQuery(
dataSource,
`SELECT ROW_TO_JSON(TABLE_DATA) FROM (SELECT * FROM %I.%I %s %s) TABLE_DATA`,
schema,
table,
orderByClause,
limitAndOffsetClause,
),
getPreparedReadOnlyHasuraQuery(
dataSource,
`SELECT ROW_TO_JSON(TABLE_DATA) FROM (\
@@ -178,12 +132,6 @@ export default async function fetchTable({
schema,
table,
),
getPreparedReadOnlyHasuraQuery(
dataSource,
`SELECT COUNT(*) FROM %I.%I`,
schema,
table,
),
],
type: 'bulk',
version: 1,
@@ -207,8 +155,6 @@ export default async function fetchTable({
if (schemaNotFound || tableNotFound) {
return {
columns: [],
rows: [],
numberOfRows: 0,
foreignKeyRelations: [],
metadata: { schema, table, schemaNotFound, tableNotFound },
};
@@ -220,8 +166,6 @@ export default async function fetchTable({
) {
return {
columns: [],
rows: [],
numberOfRows: 0,
foreignKeyRelations: [],
metadata: { schema, table, columnsNotFound: true },
};
@@ -237,9 +181,7 @@ export default async function fetchTable({
}
const [, ...rawColumns] = responseData[0].result;
const [, ...rawData] = responseData[1].result;
const [, ...rawConstraints] = responseData[2].result;
const [, ...[rawAggregate]] = responseData[3].result;
const [, ...rawConstraints] = responseData[1].result;
const foreignKeyRelationMap = new Map<string, string>();
const uniqueKeyConstraintMap = new Map<string, string[]>();
@@ -323,13 +265,8 @@ export default async function fetchTable({
} as NormalizedQueryDataRow;
})
.sort((a, b) => a.ordinal_position - b.ordinal_position);
return {
columns,
rows: rawData.map((rawRow) =>
JSON.parse(rawRow),
) as NormalizedQueryDataRow[],
foreignKeyRelations: flatForeignKeyRelations,
numberOfRows: rawAggregate ? parseInt(rawAggregate, 10) : 0,
};
}

View File

@@ -0,0 +1,147 @@
import type { DataGridFilter } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
import type {
MutationOrQueryBaseOptions,
NormalizedQueryDataRow,
OrderBy,
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
import { getPreparedReadOnlyHasuraQuery } from '@/features/orgs/projects/database/dataGrid/utils/hasuraQueryHelpers';
import { isNotEmptyValue } from '@/lib/utils';
export interface FetchTableRowsOptions extends MutationOrQueryBaseOptions {
/**
* Name of the columns to fetch
*/
columnNames: string[];
/**
* Limit of rows to fetch.
*/
limit: number;
/**
* Offset of rows to fetch.
*/
offset: number;
/**
* Ordering configuration.
*
* @default []
*/
orderBy: OrderBy[];
/**
* Filtering configuration.
*
* @default []
*/
filters: DataGridFilter[];
}
export type FetchTableRowsResult = {
error?: string | null;
rows: NormalizedQueryDataRow[];
numberOfRows: number;
};
function createRowQuery({
columnNames,
limit,
offset,
orderBy,
filters,
schema,
table,
dataSource,
}: FetchTableRowsOptions) {
return {
type: 'select',
args: {
source: dataSource,
table: { schema, name: table },
columns: columnNames,
// TODO: create function
where: {
$and: filters?.map(({ column, op, value }) => ({
[column]: {
[op]: op === '$in' || op === '$nin' ? JSON.parse(value) : value,
},
})),
},
offset,
limit,
order_by:
orderBy?.map((ob) => ({
column: ob.columnName,
type: ob.mode.toLocaleLowerCase(),
})) ?? [],
},
};
}
async function fetchTableRows({
columnNames,
limit,
offset,
orderBy,
filters,
adminSecret,
dataSource,
appUrl,
table,
schema,
}: FetchTableRowsOptions): Promise<FetchTableRowsResult> {
const body = {
type: 'bulk',
args: [
createRowQuery({
columnNames,
limit,
offset,
orderBy,
filters,
dataSource,
table,
schema,
appUrl,
adminSecret,
}),
getPreparedReadOnlyHasuraQuery(
dataSource,
`SELECT COUNT(*) FROM %I.%I`,
schema,
table,
),
],
};
const response = await fetch(`${appUrl}/v2/query`, {
method: 'POST',
headers: {
'x-hasura-admin-secret': adminSecret,
},
body: JSON.stringify(body),
});
const responseBody = await response.json();
if (isNotEmptyValue(responseBody.error)) {
return {
rows: [],
error: responseBody.error,
numberOfRows: 0,
};
}
const [
rows,
{
result: [, [maxNumberOfRows]],
},
] = responseBody;
return {
rows,
error: null,
numberOfRows: isNotEmptyValue(filters)
? rows.length
: Number(maxNumberOfRows),
};
}
export default fetchTableRows;

View File

@@ -0,0 +1 @@
export { default as useTableRows } from './useTableRows';

View File

@@ -0,0 +1,66 @@
import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/generateAppServiceUrl';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { isNotEmptyValue } from '@/lib/utils';
import { getHasuraAdminSecret } from '@/utils/env';
import {
useQuery,
type QueryKey,
type UseQueryOptions,
} from '@tanstack/react-query';
import { useRouter } from 'next/router';
import type {
FetchTableRowsOptions,
FetchTableRowsResult,
} from './fetchTableRows';
import fetchTableRows from './fetchTableRows';
export interface UseTableRowsQueryOptions
extends Pick<
FetchTableRowsOptions,
'limit' | 'filters' | 'offset' | 'orderBy' | 'columnNames'
> {
/**
* Props passed to the underlying query hook.
*/
queryOptions?: UseQueryOptions;
}
function useTableRows(
queryKey: QueryKey,
{ queryOptions, ...options }: UseTableRowsQueryOptions,
) {
const { project } = useProject();
const {
query: { dataSourceSlug, schemaSlug, tableSlug },
isReady,
} = useRouter();
const dependenciesLoaded =
isNotEmptyValue(project) && isNotEmptyValue(options.columnNames) && isReady;
return useQuery<FetchTableRowsResult>(queryKey, {
queryFn: () => {
const appUrl = isNotEmptyValue(project)
? generateAppServiceUrl(project!.subdomain, project!.region, 'hasura')
: '';
return fetchTableRows({
appUrl,
dataSource: dataSourceSlug as string,
schema: schemaSlug as string,
table: tableSlug as string,
adminSecret:
process.env.NEXT_PUBLIC_ENV === 'dev'
? getHasuraAdminSecret()
: project!.config!.hasura.adminSecret,
...options,
});
},
retry: false,
keepPreviousData: true,
...(queryOptions && { queryOptions }),
enabled: isNotEmptyValue(queryOptions?.enabled)
? queryOptions.enabled && dependenciesLoaded
: dependenciesLoaded,
});
}
export default useTableRows;

View File

@@ -0,0 +1,37 @@
import type { DataGridFilter } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
import { isEmptyValue } from '@/lib/utils';
export const DATA_GRID_FILTER_STORAGE_KEY = 'nhost_data_grid_filter_storage';
class PersistenDataGrdiFilterStorage {
private static getAllStoredData(): Record<string, DataGridFilter[]> {
const storedData = localStorage.getItem(DATA_GRID_FILTER_STORAGE_KEY);
if (isEmptyValue(storedData)) {
return {};
}
const allStoredData = JSON.parse(storedData as string);
return allStoredData;
}
static getDataGridFilters(tablePath: string): DataGridFilter[] {
const allStoredData = PersistenDataGrdiFilterStorage.getAllStoredData();
return allStoredData[tablePath] ?? [];
}
static saveDataGridFilters(tablePath: string, filters: DataGridFilter[]) {
const allStoredData = PersistenDataGrdiFilterStorage.getAllStoredData();
const updatedAllStoredData: Record<string, DataGridFilter[]> = {
...allStoredData,
[tablePath]: filters,
};
localStorage.setItem(
DATA_GRID_FILTER_STORAGE_KEY,
JSON.stringify(updatedAllStoredData),
);
}
}
export default PersistenDataGrdiFilterStorage;

View File

@@ -67,6 +67,8 @@ export function getPreparedReadOnlyHasuraQuery(
...args,
);
// console.log({ preparedHasuraQuery });
return {
...preparedHasuraQuery,
args: {

View File

@@ -21,23 +21,22 @@ import { ReplicasFormSection } from '@/features/orgs/projects/services/component
import { StorageFormSection } from '@/features/orgs/projects/services/components/ServiceForm/components/StorageFormSection';
import {
defaultServiceFormValues,
validationSchema,
type Port,
type ServiceFormProps,
type ServiceFormValues,
} from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { getFormattedServiceConfig } from '@/features/orgs/projects/services/utils/getFormattedServiceConfig';
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
import {
useInsertRunServiceConfigMutation,
useReplaceRunServiceConfigMutation,
type ConfigRunServiceConfigInsertInput,
} from '@/utils/__generated__/graphql';
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
import { copy } from '@/utils/copy';
import { removeTypename } from '@/utils/helpers';
import { yupResolver } from '@hookform/resolvers/yup';
import { useEffect, useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
@@ -69,14 +68,7 @@ export default function ServiceForm({
useState<Error | null>(null);
const form = useForm<ServiceFormValues>({
defaultValues: initialData ?? {
compute: {
cpu: 62,
memory: 128,
},
replicas: 1,
autoscaler: null,
},
defaultValues: initialData ?? defaultServiceFormValues,
reValidateMode: 'onSubmit',
resolver: yupResolver(validationSchema),
});
@@ -142,66 +134,8 @@ export default function ServiceForm({
onDirtyStateChange(isDirty, location);
}, [isDirty, location, onDirtyStateChange]);
const getFormattedConfig = (values: ServiceFormValues) => {
// Remove any __typename property from the values
const sanitizedValues = removeTypename(values) as ServiceFormValues;
const sanitizedInitialDataPorts: Port[] = initialData?.ports
? removeTypename(initialData.ports)
: [];
const config: ConfigRunServiceConfigInsertInput = {
name: sanitizedValues.name,
image: {
image: sanitizedValues.image,
pullCredentials: sanitizedValues.pullCredentials,
},
command: sanitizedValues.command?.map((arg) => arg.argument),
resources: {
compute: {
cpu: sanitizedValues.compute?.cpu,
memory: sanitizedValues.compute?.memory,
},
storage: sanitizedValues.storage?.map((item) => ({
name: item.name,
path: item.path,
capacity: item.capacity,
})),
replicas: sanitizedValues.replicas,
autoscaler: sanitizedValues.autoscaler
? {
maxReplicas: sanitizedValues.autoscaler?.maxReplicas,
}
: null,
},
environment: sanitizedValues.environment?.map((item) => ({
name: item.name,
value: item.value,
})),
ports: sanitizedValues.ports?.map((item) => ({
port: item.port,
type: item.type,
publish: item.publish,
ingresses: item.ingresses as any, // cannot be changed on the UI always null type checking can be skipped.
rateLimit:
sanitizedInitialDataPorts.find(
(port) => port.port === item.port && port.type === item.type,
)?.rateLimit ?? (null as any), // cannot be changed on the UI always null type checking can be skipped.
})),
healthCheck: sanitizedValues.healthCheck
? {
port: sanitizedValues.healthCheck?.port,
initialDelaySeconds:
sanitizedValues.healthCheck?.initialDelaySeconds,
probePeriodSeconds: sanitizedValues.healthCheck?.probePeriodSeconds,
}
: null,
};
return config;
};
const createOrUpdateService = async (values: ServiceFormValues) => {
const config = getFormattedConfig(values);
const config = getFormattedServiceConfig({ values, initialData });
if (serviceID) {
// Update service config
@@ -292,7 +226,10 @@ export default function ServiceForm({
};
const copyConfig = () => {
const config = getFormattedConfig(formValues);
const config = getFormattedServiceConfig({
values: formValues,
initialData,
});
const base64Config = btoa(JSON.stringify(config));

View File

@@ -87,6 +87,15 @@ export type ServiceFormInitialData = Omit<ServiceFormValues, 'ports'> & {
}[];
};
export const defaultServiceFormValues = {
compute: {
cpu: 62,
memory: 128,
},
replicas: 1,
autoscaler: null,
};
export interface ServiceFormProps extends DialogFormProps {
/**
* To use in conjunction with initialData to allow for updating the service

View File

@@ -15,7 +15,10 @@ import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatfo
import { type RunService } from '@/features/orgs/projects/common/hooks/useRunServices';
import { ServiceForm } from '@/features/orgs/projects/services/components/ServiceForm';
import { type PortTypes } from '@/features/orgs/projects/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
import type { ServiceFormInitialData } from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
import {
defaultServiceFormValues,
type ServiceFormInitialData,
} from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
import { copy } from '@/utils/copy';
import { formatDistanceToNow } from 'date-fns';
@@ -74,12 +77,15 @@ export default function ServicesList({
ingresses: item.ingresses,
rateLimit: item.rateLimit,
})),
compute: service.config?.resources?.compute ?? {
cpu: 62,
memory: 128,
},
replicas: service.config?.resources?.replicas,
autoscaler: service?.config?.resources?.autoscaler,
compute:
service.config?.resources?.compute ??
defaultServiceFormValues.compute,
replicas:
service.config?.resources?.replicas ??
defaultServiceFormValues.replicas,
autoscaler:
service?.config?.resources?.autoscaler ??
defaultServiceFormValues.autoscaler,
storage: service.config?.resources?.storage,
} as ServiceFormInitialData
}

View File

@@ -0,0 +1,96 @@
import { PortTypes } from '@/features/orgs/projects/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
import getFormattedServiceConfig from './getFormattedServiceConfig';
describe('getFormattedServiceConfig', () => {
it('pghero config should be formatted correctly', () => {
const pgheroFormValues = {
name: 'pghero',
image: 'docker.io/ankane/pghero:latest',
command: [],
resources: {
compute: {
cpu: 125,
memory: 256,
},
storage: [],
replicas: 1,
},
environment: [
{
name: 'DATABASE_URL',
value:
'postgres://postgres:[PASSWORD]@postgres-service:5432/[SUBDOMAIN]?sslmode=disable',
},
{
name: 'PGHERO_USERNAME',
value: '[USER]',
},
{
name: 'PGHERO_PASSWORD',
value: '[PASSWORD]',
},
],
ports: [
{
port: 8080,
type: PortTypes.HTTP,
publish: true,
},
],
autoscaler: null,
compute: {
cpu: 125,
memory: 256,
},
replicas: 1,
storage: [],
};
const formattedConfig = getFormattedServiceConfig({
values: pgheroFormValues,
});
const expected = {
name: 'pghero',
image: {
image: 'docker.io/ankane/pghero:latest',
},
command: [],
resources: {
compute: {
cpu: 125,
memory: 256,
},
storage: [],
replicas: 1,
autoscaler: null,
},
environment: [
{
name: 'DATABASE_URL',
value:
'postgres://postgres:[PASSWORD]@postgres-service:5432/[SUBDOMAIN]?sslmode=disable',
},
{
name: 'PGHERO_USERNAME',
value: '[USER]',
},
{
name: 'PGHERO_PASSWORD',
value: '[PASSWORD]',
},
],
ports: [
{
port: 8080,
type: 'http',
publish: true,
rateLimit: null,
},
],
healthCheck: null,
};
expect(formattedConfig).toEqual(expected);
});
});

View File

@@ -0,0 +1,72 @@
import type {
Port,
ServiceFormInitialData,
ServiceFormValues,
} from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
import type { ConfigRunServiceConfigInsertInput } from '@/utils/__generated__/graphql';
import { removeTypename } from '@/utils/helpers';
export interface GetFormattedServiceConfigProps {
values: ServiceFormValues;
initialData?: ServiceFormInitialData;
}
export default function getFormattedServiceConfig({
values,
initialData,
}: GetFormattedServiceConfigProps) {
// Remove any __typename property from the values
const sanitizedValues = removeTypename(values) as ServiceFormValues;
const sanitizedInitialDataPorts: Port[] = initialData?.ports
? removeTypename(initialData.ports)
: [];
const config: ConfigRunServiceConfigInsertInput = {
name: sanitizedValues.name,
image: {
image: sanitizedValues.image,
pullCredentials: sanitizedValues.pullCredentials,
},
command: sanitizedValues.command?.map((arg) => arg.argument),
resources: {
compute: {
cpu: sanitizedValues.compute?.cpu,
memory: sanitizedValues.compute?.memory,
},
storage: sanitizedValues.storage?.map((item) => ({
name: item.name,
path: item.path,
capacity: item.capacity,
})),
replicas: sanitizedValues.replicas,
autoscaler: sanitizedValues.autoscaler
? {
maxReplicas: sanitizedValues.autoscaler?.maxReplicas,
}
: null,
},
environment: sanitizedValues.environment?.map((item) => ({
name: item.name,
value: item.value,
})),
ports: sanitizedValues.ports?.map((item) => ({
port: item.port,
type: item.type,
publish: item.publish,
ingresses: item.ingresses as any, // cannot be changed on the UI always null type checking can be skipped.
rateLimit:
sanitizedInitialDataPorts.find(
(port) => port.port === item.port && port.type === item.type,
)?.rateLimit ?? (null as any), // cannot be changed on the UI always null type checking can be skipped.
})),
healthCheck: sanitizedValues.healthCheck
? {
port: sanitizedValues.healthCheck?.port,
initialDelaySeconds: sanitizedValues.healthCheck?.initialDelaySeconds,
probePeriodSeconds: sanitizedValues.healthCheck?.probePeriodSeconds,
}
: null,
};
return config;
}

View File

@@ -0,0 +1 @@
export { default as getFormattedServiceConfig } from './getFormattedServiceConfig';

View File

@@ -0,0 +1 @@
export { default as parseConfigFromInstallLink } from './parseConfigFromInstallLink';

View File

@@ -0,0 +1,156 @@
import { describe, expect, it } from 'vitest';
import parseConfigFromInstallLink from './parseConfigFromInstallLink';
describe('parseConfigFromInstallLink', () => {
it('pghero config without autoscaler should be formatted correctly', () => {
const pgheroBase64Config =
'eyJuYW1lIjoicGdoZXJvIiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vYW5rYW5lL3BnaGVybzpsYXRlc3QifSwiY29tbWFuZCI6W10sInJlc291cmNlcyI6eyJjb21wdXRlIjp7ImNwdSI6MTI1LCJtZW1vcnkiOjI1Nn0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MX0sImVudmlyb25tZW50IjpbeyJuYW1lIjoiREFUQUJBU0VfVVJMIiwidmFsdWUiOiJwb3N0Z3JlczovL3Bvc3RncmVzOltQQVNTV09SRF1AcG9zdGdyZXMtc2VydmljZTo1NDMyL1tTVUJET01BSU5dP3NzbG1vZGU9ZGlzYWJsZSJ9LHsibmFtZSI6IlBHSEVST19VU0VSTkFNRSIsInZhbHVlIjoiW1VTRVJdIn0seyJuYW1lIjoiUEdIRVJPX1BBU1NXT1JEIiwidmFsdWUiOiJbUEFTU1dPUkRdIn1dLCJwb3J0cyI6W3sicG9ydCI6ODA4MCwidHlwZSI6Imh0dHAiLCJwdWJsaXNoIjp0cnVlfV19';
const config = parseConfigFromInstallLink(pgheroBase64Config);
const expected = {
name: 'pghero',
image: 'docker.io/ankane/pghero:latest',
command: [],
resources: {
compute: {
cpu: 125,
memory: 256,
},
storage: [],
replicas: 1,
},
environment: [
{
name: 'DATABASE_URL',
value:
'postgres://postgres:[PASSWORD]@postgres-service:5432/[SUBDOMAIN]?sslmode=disable',
},
{
name: 'PGHERO_USERNAME',
value: '[USER]',
},
{
name: 'PGHERO_PASSWORD',
value: '[PASSWORD]',
},
],
ports: [
{
port: 8080,
type: 'http',
publish: true,
},
],
autoscaler: null,
compute: {
cpu: 125,
memory: 256,
},
replicas: 1,
storage: [],
};
expect(config).toEqual(expected);
});
it('antivirus config without autoscaler should be formatted correctly', () => {
const antivirusBase64Config =
'eyJuYW1lIjoiY2xhbWF2IiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vbmhvc3QvY2xhbWF2OjAuMS4xIn0sImNvbW1hbmQiOltdLCJyZXNvdXJjZXMiOnsiY29tcHV0ZSI6eyJjcHUiOjEwMDAsIm1lbW9yeSI6MjA0OH0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MX0sImVudmlyb25tZW50IjpbXSwicG9ydHMiOlt7InBvcnQiOiIzMzEwIiwidHlwZSI6InRjcCIsInB1Ymxpc2giOmZhbHNlfV19';
const config = parseConfigFromInstallLink(antivirusBase64Config);
const expected = {
name: 'clamav',
image: 'docker.io/nhost/clamav:0.1.1',
command: [],
resources: {
compute: {
cpu: 1000,
memory: 2048,
},
storage: [],
replicas: 1,
},
environment: [],
ports: [
{
port: '3310',
type: 'tcp',
publish: false,
},
],
autoscaler: null,
compute: {
cpu: 1000,
memory: 2048,
},
replicas: 1,
storage: [],
};
expect(config).toEqual(expected);
});
it('invalid config should throw an error', () => {
const invalidBase64Config = 'invalid';
expect(() => parseConfigFromInstallLink(invalidBase64Config)).toThrow();
});
it('pghero config with autoscaler should be formatted correctly', () => {
const pgheroWithAutoscalerBase64 =
'eyJuYW1lIjoicGdoZXJvIiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vYW5rYW5lL3BnaGVybzpsYXRlc3QifSwiY29tbWFuZCI6W10sInJlc291cmNlcyI6eyJjb21wdXRlIjp7ImNwdSI6MTI1LCJtZW1vcnkiOjI1Nn0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MSwiYXV0b3NjYWxlciI6eyJtYXhSZXBsaWNhcyI6MTF9fSwiZW52aXJvbm1lbnQiOlt7Im5hbWUiOiJEQVRBQkFTRV9VUkwiLCJ2YWx1ZSI6InBvc3RncmVzOi8vcG9zdGdyZXM6W1BBU1NXT1JEXUBwb3N0Z3Jlcy1zZXJ2aWNlOjU0MzIvW1NVQkRPTUFJTl0/c3NsbW9kZT1kaXNhYmxlIn0seyJuYW1lIjoiUEdIRVJPX1VTRVJOQU1FIiwidmFsdWUiOiJbVVNFUl0ifSx7Im5hbWUiOiJQR0hFUk9fUEFTU1dPUkQiLCJ2YWx1ZSI6IltQQVNTV09SRF0ifV0sInBvcnRzIjpbeyJwb3J0Ijo4MDgwLCJ0eXBlIjoiaHR0cCIsInB1Ymxpc2giOnRydWUsInJhdGVMaW1pdCI6bnVsbH1dLCJoZWFsdGhDaGVjayI6bnVsbH0=';
const config = parseConfigFromInstallLink(pgheroWithAutoscalerBase64);
const expected = {
name: 'pghero',
image: 'docker.io/ankane/pghero:latest',
command: [],
resources: {
compute: {
cpu: 125,
memory: 256,
},
storage: [],
replicas: 1,
autoscaler: {
maxReplicas: 11,
},
},
environment: [
{
name: 'DATABASE_URL',
value:
'postgres://postgres:[PASSWORD]@postgres-service:5432/[SUBDOMAIN]?sslmode=disable',
},
{
name: 'PGHERO_USERNAME',
value: '[USER]',
},
{
name: 'PGHERO_PASSWORD',
value: '[PASSWORD]',
},
],
ports: [
{
port: 8080,
type: 'http',
publish: true,
},
],
autoscaler: {
maxReplicas: 11,
},
compute: {
cpu: 125,
memory: 256,
},
replicas: 1,
storage: [],
};
expect(config).toEqual(expected);
});
});

View File

@@ -0,0 +1,48 @@
import { type RunServiceConfig } from '@/features/orgs/projects/common/hooks/useRunServices';
import { type PortTypes } from '@/features/orgs/projects/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
import {
defaultServiceFormValues,
type ServiceFormInitialData,
} from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
export default function parseConfigFromInstallLink(
base64Config: string,
): ServiceFormInitialData {
const decodedConfig = atob(base64Config);
const parsedConfig: RunServiceConfig = JSON.parse(decodedConfig);
const initialData = {
...parsedConfig,
autoscaler:
parsedConfig?.resources?.autoscaler ??
defaultServiceFormValues.autoscaler,
compute:
parsedConfig?.resources?.compute ?? defaultServiceFormValues.compute,
image: parsedConfig?.image?.image,
command: parsedConfig?.command?.map((arg) => ({
argument: arg,
})),
environment:
parsedConfig?.environment?.map((env) => ({
name: env.name,
value: env.value,
})) ?? undefined,
healthCheck: parsedConfig?.healthCheck
? {
port: parsedConfig.healthCheck.port ?? 3000,
initialDelaySeconds:
parsedConfig.healthCheck.initialDelaySeconds ?? 30,
probePeriodSeconds: parsedConfig.healthCheck.probePeriodSeconds ?? 60,
}
: undefined,
ports:
parsedConfig?.ports?.map((item) => ({
port: item.port ?? 3000,
type: item.type as PortTypes,
publish: Boolean(item.publish),
})) ?? [],
replicas: parsedConfig?.resources?.replicas,
storage: parsedConfig?.resources?.storage ?? undefined,
};
return initialData;
}

View File

@@ -9,7 +9,7 @@ import type { DataGridHeaderProps } from '@/features/orgs/projects/storage/dataG
import { DataGridHeader } from '@/features/orgs/projects/storage/dataGrid/components/DataGridHeader';
import { DataTableDesignProvider } from '@/features/orgs/projects/storage/dataGrid/providers/DataTableDesignProvider';
import { cn } from '@/lib/utils';
import type { ForwardedRef } from 'react';
import type { ForwardedRef, ReactNode } from 'react';
import { forwardRef, useEffect, useRef } from 'react';
import { mergeRefs } from 'react-merge-refs';
import type { Column, Row, SortingRule, TableOptions } from 'react-table';
@@ -31,7 +31,7 @@ export interface DataGridProps<TColumnData extends object>
*
* @default null
*/
emptyStateMessage?: string;
emptyStateMessage?: ReactNode;
/**
* Additional configuration options for the `react-table` hook.
*/
@@ -71,6 +71,10 @@ export interface DataGridProps<TColumnData extends object>
* Determines whether the Grid is used for displaying files.
*/
isFileDataGrid?: boolean;
/**
* Determines whether rows are being fetched or not
*/
isFetching?: boolean;
}
function DataGrid<TColumnData extends object>(
@@ -89,6 +93,7 @@ function DataGrid<TColumnData extends object>(
loading,
className,
isFileDataGrid,
isFetching,
}: DataGridProps<TColumnData>,
ref: ForwardedRef<HTMLDivElement>,
) {
@@ -151,12 +156,19 @@ function DataGrid<TColumnData extends object>(
)}
>
<DataGridFrame>
<DataGridHeader {...headerProps} />
<DataGridBody
isFileDataGrid={isFileDataGrid}
emptyStateMessage={emptyStateMessage}
loading={loading}
/>
<div className="relative h-full">
<DataGridHeader {...headerProps} />
{isFetching && (
<div className="absolute top-0 z-50 flex h-full w-full justify-center bg-[rgba(0,0,0,.5)]">
<Spinner />
</div>
)}
<DataGridBody
isFileDataGrid={isFileDataGrid}
emptyStateMessage={emptyStateMessage}
loading={loading}
/>
</div>
</DataGridFrame>
</div>
)}

View File

@@ -22,9 +22,9 @@ class PersistenDataTableConfigurationStorage {
if (isEmptyValue(storedData)) {
return {};
}
const allHiddenColumns = JSON.parse(storedData as string);
const allStoredData = JSON.parse(storedData as string);
return allHiddenColumns;
return allStoredData;
}
static getHiddenColumns(tablePath: string): string[] {

View File

@@ -4,6 +4,7 @@ import { OrgLayout } from '@/features/orgs/layout/OrgLayout';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { useTablePath } from '@/features/orgs/projects/database/common/hooks/useTablePath';
import { DataBrowserGrid } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid';
import { DataGridFilterProvider } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserGrid/DataGridFilterProvider';
import { DataBrowserSidebar } from '@/features/orgs/projects/database/dataGrid/components/DataBrowserSidebar';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import type { ReactElement } from 'react';
@@ -31,7 +32,9 @@ export default function DataBrowserTableDetailsPage() {
return (
<RetryableErrorBoundary>
<DataBrowserGrid sortBy={sortBy} onSort={handleSortByChange} />
<DataGridFilterProvider>
<DataBrowserGrid sortBy={sortBy} onSort={handleSortByChange} />
</DataGridFilterProvider>
</RetryableErrorBoundary>
);
}

View File

@@ -11,16 +11,12 @@ import { ServicesIcon } from '@/components/ui/v2/icons/ServicesIcon';
import { Text } from '@/components/ui/v2/Text';
import { OrgLayout } from '@/features/orgs/layout/OrgLayout';
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import {
useRunServices,
type RunServiceConfig,
} from '@/features/orgs/projects/common/hooks/useRunServices';
import { useRunServices } from '@/features/orgs/projects/common/hooks/useRunServices';
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import { ServiceForm } from '@/features/orgs/projects/services/components/ServiceForm';
import { type PortTypes } from '@/features/orgs/projects/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
import type { ServiceFormInitialData } from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
import { ServicesList } from '@/features/orgs/projects/services/components/ServicesList';
import { parseConfigFromInstallLink } from '@/features/orgs/projects/services/utils/parseConfigFromInstallLink';
import { useRouter } from 'next/router';
import { useCallback, useEffect, type ReactElement } from 'react';
@@ -48,29 +44,7 @@ export default function RunPage() {
(base64Config: string) => {
if (router.query?.config) {
try {
const decodedConfig = atob(base64Config);
const parsedConfig: RunServiceConfig = JSON.parse(decodedConfig);
const initialData = {
...parsedConfig,
autoscaler: parsedConfig?.resources?.autoscaler ?? {
maxReplicas: 0,
},
compute: parsedConfig?.resources?.compute ?? {
cpu: 62,
memory: 128,
},
image: parsedConfig?.image?.image,
command: parsedConfig?.command?.map((arg) => ({
argument: arg,
})),
ports: parsedConfig?.ports?.map((item) => ({
port: item.port,
type: item.type as PortTypes,
publish: item.publish,
})),
replicas: parsedConfig?.resources?.replicas,
storage: parsedConfig?.resources?.storage,
} as ServiceFormInitialData;
const initialData = parseConfigFromInstallLink(base64Config);
openDrawer({
title: (

View File

@@ -126,6 +126,7 @@
"icon": "at",
"pages": [
"products/auth/providers/overview",
"products/auth/providers/sign-in-provider",
"products/auth/providers/tokens",
"products/auth/providers/connect",
"products/auth/providers/idtokens",

View File

@@ -27,6 +27,9 @@
"openapi-types": "*"
}
}
},
"overrides": {
"js-yaml@<=4.1.0": ">=4.1.1"
}
}
}

View File

@@ -29,7 +29,7 @@ After authorizing the GitHub integration, you'll need to tell Nhost a couple of
### Base Directory
This is the folder in your repository where your Nhost folder lives. If your Nhost foder is in the root of your repository, you can leave this as `/`. If it is in a subfolder (like `/backend`), specify that path here.
This is the folder in your repository where your Nhost folder lives. If your Nhost folder is in the root of your repository, you can leave this as `/`. If it is in a subfolder (like `/backend`), specify that path here.
### Deployment Branch

48
docs/pnpm-lock.yaml generated
View File

@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
js-yaml@<=4.1.0: '>=4.1.1'
packageExtensionsChecksum: sha256-4+NJJHoeDEOtWI2UxgTNLimXyrOojBs00S85/9Babm0=
importers:
@@ -936,9 +939,6 @@ packages:
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -2106,12 +2106,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
hasBin: true
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsep@1.4.0:
@@ -3126,9 +3122,6 @@ packages:
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
stack-utils@2.0.6:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
@@ -3599,7 +3592,7 @@ snapshots:
ajv-errors: 3.0.0(ajv@8.17.1)
ajv-formats: 2.1.1(ajv@8.17.1)
avsc: 5.7.9
js-yaml: 4.1.0
js-yaml: 4.1.1
jsonpath-plus: 10.3.0
node-fetch: 2.6.7
transitivePeerDependencies:
@@ -3937,7 +3930,7 @@ snapshots:
gray-matter: 4.0.3
ink: 6.3.1(@types/react@19.1.12)(react@19.2.0)
inquirer: 12.9.6(@types/node@24.7.0)
js-yaml: 4.1.0
js-yaml: 4.1.1
mdast: 3.0.0
mdast-util-mdx-jsx: 3.2.0
react: 19.2.0
@@ -3979,7 +3972,7 @@ snapshots:
hast-util-to-html: 9.0.5
hast-util-to-text: 4.0.2
hex-rgb: 5.0.0
js-yaml: 4.1.0
js-yaml: 4.1.1
lodash: 4.17.21
mdast: 3.0.0
mdast-util-from-markdown: 2.0.2
@@ -4094,7 +4087,7 @@ snapshots:
favicons: 7.2.0
fs-extra: 11.3.2
gray-matter: 4.0.3
js-yaml: 4.1.0
js-yaml: 4.1.1
mdast: 3.0.0
openapi-types: 12.1.3
sharp: 0.33.5
@@ -4131,7 +4124,7 @@ snapshots:
ink: 6.3.1(@types/react@19.1.12)(react@19.2.0)
ink-spinner: 5.0.0(ink@6.3.1(@types/react@19.1.12)(react@19.2.0))(react@19.2.0)
is-online: 10.0.0
js-yaml: 4.1.0
js-yaml: 4.1.1
mdast: 3.0.0
openapi-types: 12.1.3
react: 19.2.0
@@ -4161,7 +4154,7 @@ snapshots:
'@mintlify/openapi-parser': 0.0.8
fs-extra: 11.3.2
hast-util-to-mdast: 10.1.2
js-yaml: 4.1.0
js-yaml: 4.1.1
mdast-util-mdx-jsx: 3.2.0
neotraverse: 0.6.18
openapi-types: 12.1.3
@@ -4196,7 +4189,7 @@ snapshots:
'@mintlify/mdx': 3.0.0(@radix-ui/react-popover@1.1.15(@types/react@19.1.12)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@types/react@19.1.12)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.2)
'@mintlify/models': 0.0.233
arktype: 2.1.22
js-yaml: 4.1.0
js-yaml: 4.1.1
lcm: 0.0.3
lodash: 4.17.21
object-hash: 3.0.0
@@ -4791,10 +4784,6 @@ snapshots:
arg@5.0.2: {}
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3
argparse@2.0.1: {}
aria-hidden@1.2.6:
@@ -5095,7 +5084,7 @@ snapshots:
dependencies:
env-paths: 2.2.1
import-fresh: 3.3.1
js-yaml: 4.1.0
js-yaml: 4.1.1
parse-json: 5.2.0
optionalDependencies:
typescript: 5.9.2
@@ -5703,7 +5692,7 @@ snapshots:
gray-matter@4.0.3:
dependencies:
js-yaml: 3.14.1
js-yaml: 4.1.1
kind-of: 6.0.3
section-matter: 1.0.0
strip-bom-string: 1.0.0
@@ -6185,12 +6174,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.14.1:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
js-yaml@4.1.0:
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
@@ -7673,8 +7657,6 @@ snapshots:
space-separated-tokens@2.0.2: {}
sprintf-js@1.0.3: {}
stack-utils@2.0.6:
dependencies:
escape-string-regexp: 2.0.0

View File

@@ -86,14 +86,4 @@ icon: apple
## Sign In Users
Use the [Nhost JavaScript client](/reference/javascript) to sign in users:
```js
nhost.auth.signIn({
provider: 'apple'
})
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured Apple as an OAuth provider in Nhost, you can sign in users using the Apple provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -36,14 +36,4 @@ Find the Redirect URL in your project settings -> Sign In Methods after enabling
## User Sign-In
Use the [Nhost JavaScript client](/reference/javascript) to sign in users:
```js
nhost.auth.signIn({
provider: 'azuread'
})
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured Azure AD as an OAuth provider in Nhost, you can sign in users using the Azure AD provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -39,13 +39,4 @@ Once saved, Bitbucket will show you a **Key (Client ID)** and a **Secret (Client
## Sign In Users
Use the [Nhost JavaScript client](/reference/javascript) to sign in users with Bitbucket:
```js
nhost.auth.signIn({
provider: "bitbucket",
});
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured Bitbucket as an OAuth provider in Nhost, you can sign in users using the Bitbucket provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -34,14 +34,4 @@ icon: discord
## Sign In Users
Use the [Nhost JavaScript client](/reference/javascript) to sign in users:
```js
nhost.auth.signIn({
provider: 'discord'
})
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured Discord as an OAuth provider in Nhost, you can sign in users using the Discord provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -34,14 +34,4 @@ Find the Redirect URL in your project settings -> Sign In Methods after enabling
## User Sign-In
Use the [Nhost JavaScript client](/reference/javascript) to sign in users:
```js
nhost.auth.signIn({
provider: 'azuread'
})
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured Entra ID as an OAuth provider in Nhost, you can sign in users using the Entra ID provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -57,14 +57,4 @@ To make sure we can fetch all user data (email, profile picture and name). For t
## Sign In Users
Use the [Nhost JavaScript client](/reference/javascript) to sign in users:
```js
nhost.auth.signIn({
provider: 'facebook'
})
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured Facebook as an OAuth provider in Nhost, you can sign in users using the Facebook provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -42,14 +42,4 @@ icon: github
## Sign In Users
Use the [Nhost JavaScript client](/reference/javascript) to sign in users:
```js
nhost.auth.signIn({
provider: "github",
});
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured GitHub as an OAuth provider in Nhost, you can sign in users using the GitHub provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -34,14 +34,4 @@ icon: gitlab
## Sign In Users
Use the [Nhost JavaScript client](/reference/javascript) to sign in users:
```js
nhost.auth.signIn({
provider: "gitlab",
});
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured GitLab as an OAuth provider in Nhost, you can sign in users using the GitLab provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -66,14 +66,4 @@ icon: google
## Sign In Users
Use the Nhost JavaScript client to sign in users:
```js
nhost.auth.signIn({
provider: 'google'
})
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured Google as an OAuth provider in Nhost, you can sign in users using the Google provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -57,14 +57,4 @@ icon: linkedin
## Sign In Users
Use the [Nhost JavaScript client](/reference/javascript) to sign in users:
```js
nhost.auth.signIn({
provider: 'linkedin'
})
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured LinkedIn as an OAuth provider in Nhost, you can sign in users using the LinkedIn provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -0,0 +1,244 @@
---
title: Sign In with OAuth Providers
description: Learn how OAuth provider sign-in works in Nhost and how to implement it in your application.
sidebarTitle: Sign In
icon: user
---
## Overview
Nhost supports OAuth 2.0 authentication with various social providers including GitHub, Google, Apple, Discord, and more. This guide explains the OAuth sign-in flow and how to implement it in your application.
## OAuth Sign-In Flow
The OAuth authentication flow in Nhost involves several steps coordinating between your client application, Nhost Auth service, and the OAuth provider:
```mermaid
sequenceDiagram
participant Client as Client Application
participant NhostAuth as Nhost Auth Service
participant Provider as OAuth Provider
Client->>Client: User clicks "Sign in with Provider"
Client->>Client: Call nhost.auth.signInProviderURL(provider, options)
Client->>Client: Redirect to returned URL
Client->>NhostAuth: GET /v1/signin/provider/{provider}
Note over NhostAuth: Generate OAuth state & store session
NhostAuth->>Provider: 302 Redirect to provider authorization
Provider->>Provider: User authorizes application
Provider->>NhostAuth: 302 Callback with authorization code
Note over NhostAuth: Exchange code for tokens<br/>Create/update user<br/>Generate refresh token
NhostAuth->>Client: 302 Redirect to redirectTo URL
Note over Client: URL contains refreshToken<br/>or error information
Client->>Client: Extract refreshToken from URL
Client->>NhostAuth: POST /v1/token with refreshToken
NhostAuth->>Client: Return session with accessToken
Client->>Client: Store session & authenticate user
```
## Implementation Steps
### 1. Generate the Provider Sign-In URL
Use the `signInProviderURL()` method to generate the OAuth authorization URL. This method returns a URL that you'll redirect the user to:
```tsx
import { nhost } from './lib/nhost';
const handleSocialSignIn = (provider: 'github' | 'google' | 'apple') => {
// Get the current origin to build the callback URL
const origin = window.location.origin;
const redirectUrl = `${origin}/verify`;
// Generate the provider sign-in URL
const url = nhost.auth.signInProviderURL(provider, {
redirectTo: redirectUrl,
});
// Redirect the user to the OAuth provider
window.location.href = url;
};
```
### 2. OAuth Provider Authorization
When the user is redirected to the OAuth provider (e.g., GitHub, Google), they will:
1. See a consent screen asking to authorize your application
2. Grant or deny permission to access their profile information
3. Be redirected back to Nhost Auth's callback URL
### 3. Nhost Auth Callback Processing
Nhost Auth receives the callback from the OAuth provider at `/v1/signin/provider/{provider}/callback` and performs the following:
1. **Validates the OAuth state** to prevent CSRF attacks
2. **Exchanges the authorization code** for access and refresh tokens from the provider
3. **Fetches the user's profile** from the provider
4. **Creates or updates the user** in your Nhost database
5. **Generates a Nhost refresh token** for the session
6. **Redirects to your client application** at the `redirectTo` URL
### 4. Handle the Redirect
After successful authentication, Nhost redirects back to your `redirectTo` URL with query parameters. You need to handle two scenarios:
#### Success - Extract the Refresh Token
On success, the URL will contain a `refreshToken` parameter:
```
https://your-app.com/verify?refreshToken=abc123...
```
Extract this token and exchange it for a session:
```tsx
import type { ErrorResponse } from '@nhost/nhost-js/auth';
import type { FetchError } from '@nhost/nhost-js/fetch';
import { useEffect, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { nhost } from './lib/nhost';
export default function Verify() {
const navigate = useNavigate();
const location = useLocation();
const [status, setStatus] = useState<'verifying' | 'success' | 'error'>('verifying');
const [error, setError] = useState<string>('');
useEffect(() => {
const params = new URLSearchParams(location.search);
const refreshToken = params.get('refreshToken');
if (!refreshToken) {
setStatus('error');
setError('No refresh token found in URL');
return;
}
let isMounted = true;
async function processToken() {
try {
// Exchange refresh token for session
await nhost.auth.refreshToken({ refreshToken });
if (!isMounted) return;
setStatus('success');
// Redirect to the application
setTimeout(() => {
if (isMounted) navigate('/profile');
}, 1500);
} catch (err) {
const error = err as FetchError<ErrorResponse>;
if (!isMounted) return;
setStatus('error');
setError(`An error occurred during verification: ${error.message}`);
}
}
processToken();
return () => {
isMounted = false;
};
}, [location.search, navigate]);
return (
<div>
{status === 'verifying' && <p>Verifying...</p>}
{status === 'success' && <p>Successfully verified! Redirecting...</p>}
{status === 'error' && (
<div>
<p>Verification failed: {error}</p>
<button onClick={() => navigate('/signin')}>Back to Sign In</button>
</div>
)}
</div>
);
}
```
#### Error - Handle Authentication Failure
On error, the URL will contain error parameters:
```
https://your-app.com/verify?error=access_denied
```
You can handle these errors by checking for the `error` query parameter:
```tsx
const params = new URLSearchParams(location.search);
const error = params.get('error');
if (error) {
// Handle error - redirect to sign-in page with error message
navigate(`/signin?error=${encodeURIComponent(error)}`);
return;
}
```
Common error scenarios include:
- User denied authorization at the OAuth provider
- Invalid OAuth request configuration
- Error from the OAuth provider
- Provider account already linked to another user
### 5. Session Management
Once you've exchanged the refresh token for a session, the Nhost SDK automatically manages:
- **Access token** - Short-lived JWT for API requests (default: 15 minutes)
- **Refresh token** - Used to obtain new access tokens (default: 30 days)
- **Automatic token refresh** - The SDK refreshes tokens before expiration
## Security Considerations
### CSRF Protection
Nhost automatically handles CSRF protection using the OAuth `state` parameter. Each sign-in request generates a unique state value that is validated during the callback.
### Redirect URL Validation
For security, Nhost validates that the `redirectTo` URL matches either your clientUrl or one of your configured allowed redirect URLs. Configure these in your Nhost project settings.
### Custom Domains
To use your own domain for the OAuth callback URL instead of the default Nhost domain, refer to the [custom domains](/platform/cloud/custom-domains) documentation.
## Provider-Specific Setup
Each OAuth provider requires specific configuration. Refer to the provider-specific guides for detailed setup instructions:
- [Apple](/products/auth/providers/sign-in-apple)
- [Azure AD / Entra ID](/products/auth/providers/sign-in-azuread)
- [Bitbucket](/products/auth/providers/sign-in-bitbucket)
- [Discord](/products/auth/providers/sign-in-discord)
- [Facebook](/products/auth/providers/sign-in-facebook)
- [GitHub](/products/auth/providers/sign-in-github)
- [GitLab](/products/auth/providers/sign-in-gitlab)
- [Google](/products/auth/providers/sign-in-google)
- [LinkedIn](/products/auth/providers/sign-in-linkedin)
- [Spotify](/products/auth/providers/sign-in-spotify)
- [Strava](/products/auth/providers/sign-in-strava)
- [Twitch](/products/auth/providers/sign-in-twitch)
- [Windows Live](/products/auth/providers/sign-in-windowslive)
- [WorkOS](/products/auth/providers/sign-in-workos)
## API Reference
For detailed API documentation, see:
- [signInProviderURL()](/reference/javascript/nhost-js/auth#signinproviderurl) in the JavaScript SDK reference
- [GET /v1/signin/provider/{ '{provider}' }](/reference/auth/get-signin-provider-{provider}) in the API reference
- [GET /v1/signin/provider/{ '{provider}' }/callback](/reference/auth/get-signin-provider-{provider}-callback) in the API reference

View File

@@ -42,14 +42,4 @@ icon: spotify
## Sign In Users
Use the [Nhost JavaScript client](/reference/javascript) to sign in users:
```js
nhost.auth.signIn({
provider: 'spotify'
})
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured Spotify as an OAuth provider in Nhost, you can sign in users using the Spotify provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -33,14 +33,4 @@ Due to Strava API updates, email is no longer returned and is intentionally left
## Sign In Users
Use the [Nhost JavaScript client](/reference/javascript) to sign in users:
```js
nhost.auth.signIn({
provider: "strava",
});
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured Strava as an OAuth provider in Nhost, you can sign in users using the Strava provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -36,14 +36,4 @@ icon: twitch
## Sign In Users
Use the [Nhost JavaScript client](/reference/javascript) to sign in users:
```js
nhost.auth.signIn({
provider: 'twitch'
})
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured Twitch as an OAuth provider in Nhost, you can sign in users using the Twitch provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -32,14 +32,4 @@ icon: windowslive
## Sign In Users
Use the [Nhost JavaScript client](/reference/javascript) to sign in users:
```js
nhost.auth.signIn({
provider: "windowslive",
});
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured WindowsLive as an OAuth provider in Nhost, you can sign in users using the WindowsLive provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -56,14 +56,4 @@ See the [WorkOS documentation](https://workos.com/docs/) to learn more about how
## Sign In Users
Use the [Nhost JavaScript client](/reference/javascript) to sign in users:
```js
nhost.auth.signIn({
provider: 'workos'
})
```
<Note>
To use your own domain for the callback URL refer to the [custom domains](/platform/cloud/custom-domains) documentation.
</Note>
Once you've configured WorkOS as an OAuth provider in Nhost, you can sign in users using the WorkOS provider. See the [OAuth Provider Sign-In Guide](/products/auth/providers/sign-in-provider) for detailed implementation instructions including the complete OAuth flow, error handling, and session management.

View File

@@ -54,5 +54,10 @@
"@types/react": "~19.0.14",
"@types/node": "^22.15.17"
},
"private": true
"private": true,
"pnpm": {
"overrides": {
"js-yaml@<=4.1.0": ">=4.1.1"
}
}
}

View File

@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
js-yaml@<=4.1.0: '>=4.1.1'
importers:
.:
@@ -1064,9 +1067,6 @@ packages:
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -1443,11 +1443,6 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
@@ -1877,12 +1872,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
hasBin: true
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsc-safe-url@0.2.4:
@@ -2623,9 +2614,6 @@ packages:
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
engines: {node: '>=6'}
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
stack-utils@2.0.6:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
@@ -3825,7 +3813,7 @@ snapshots:
'@babel/code-frame': 7.10.4
chalk: 4.1.2
find-up: 5.0.0
js-yaml: 4.1.0
js-yaml: 4.1.1
'@isaacs/cliui@8.0.2':
dependencies:
@@ -3847,7 +3835,7 @@ snapshots:
camelcase: 5.3.1
find-up: 4.1.0
get-package-type: 0.1.0
js-yaml: 3.14.1
js-yaml: 4.1.1
resolve-from: 5.0.0
'@istanbuljs/schema@0.1.3': {}
@@ -4296,10 +4284,6 @@ snapshots:
arg@5.0.2: {}
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3
argparse@2.0.1: {}
asap@2.0.6: {}
@@ -4615,7 +4599,7 @@ snapshots:
dependencies:
import-fresh: 2.0.0
is-directory: 0.3.1
js-yaml: 3.14.1
js-yaml: 4.1.1
parse-json: 4.0.0
cross-spawn@7.0.6:
@@ -4698,8 +4682,6 @@ snapshots:
escape-string-regexp@4.0.0: {}
esprima@4.0.1: {}
etag@1.8.1: {}
event-target-shim@5.0.1: {}
@@ -5178,12 +5160,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.14.1:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
js-yaml@4.1.0:
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
@@ -6011,8 +5988,6 @@ snapshots:
split-on-first@1.1.0: {}
sprintf-js@1.0.3: {}
stack-utils@2.0.6:
dependencies:
escape-string-regexp: 2.0.0

View File

@@ -32,5 +32,10 @@
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1"
},
"pnpm": {
"overrides": {
"js-yaml@<=4.1.0": ">=4.1.1"
}
}
}

View File

@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
js-yaml@<=4.1.0: '>=4.1.1'
importers:
.:
@@ -1551,8 +1554,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsesc@3.1.0:
@@ -3103,7 +3106,7 @@ snapshots:
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
jose: 5.10.0
js-yaml: 4.1.0
js-yaml: 4.1.1
lodash: 4.17.21
scuid: 1.1.0
tslib: 2.8.1
@@ -3613,7 +3616,7 @@ snapshots:
cosmiconfig@8.3.6:
dependencies:
import-fresh: 3.3.1
js-yaml: 4.1.0
js-yaml: 4.1.1
parse-json: 5.2.0
path-type: 4.0.0
@@ -3965,7 +3968,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@4.1.0:
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1

View File

@@ -33,5 +33,10 @@
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1"
},
"pnpm": {
"overrides": {
"js-yaml@<=4.1.0": ">=4.1.1"
}
}
}

View File

@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
js-yaml@<=4.1.0: '>=4.1.1'
importers:
.:
@@ -1534,8 +1537,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsesc@3.1.0:
@@ -3029,7 +3032,7 @@ snapshots:
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
jose: 5.10.0
js-yaml: 4.1.0
js-yaml: 4.1.1
lodash: 4.17.21
scuid: 1.1.0
tslib: 2.8.1
@@ -3538,7 +3541,7 @@ snapshots:
cosmiconfig@8.3.6:
dependencies:
import-fresh: 3.3.1
js-yaml: 4.1.0
js-yaml: 4.1.1
parse-json: 5.2.0
path-type: 4.0.0
@@ -3886,7 +3889,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@4.1.0:
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1

View File

@@ -33,5 +33,10 @@
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1"
},
"pnpm": {
"overrides": {
"js-yaml@<=4.1.0": ">=4.1.1"
}
}
}

View File

@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
js-yaml@<=4.1.0: '>=4.1.1'
importers:
.:
@@ -1294,8 +1297,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsesc@3.1.0:
@@ -2500,7 +2503,7 @@ snapshots:
http-proxy-agent: 7.0.2
https-proxy-agent: 7.0.6
jose: 5.10.0
js-yaml: 4.1.0
js-yaml: 4.1.1
lodash: 4.17.21
scuid: 1.1.0
tslib: 2.8.1
@@ -2940,7 +2943,7 @@ snapshots:
cosmiconfig@8.3.6:
dependencies:
import-fresh: 3.3.1
js-yaml: 4.1.0
js-yaml: 4.1.1
parse-json: 5.2.0
path-type: 4.0.0
@@ -3267,7 +3270,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@4.1.0:
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1

View File

@@ -3,8 +3,14 @@ table:
schema: auth
is_enum: true
configuration:
column_config: {}
custom_column_names: {}
column_config:
comment:
custom_name: comment
value:
custom_name: value
custom_column_names:
comment: comment
value: value
custom_name: authRefreshTokenTypes
custom_root_fields:
delete: deleteAuthRefreshTokenTypes

View File

@@ -31,7 +31,7 @@ httpPoolSize = 100
version = 22
[auth]
version = '0.41.1'
version = '0.43.1'
[auth.elevatedPrivileges]
mode = 'disabled'
@@ -183,7 +183,7 @@ capacity = 1
[provider]
[storage]
version = '0.8.0-beta5'
version = '0.9.1'
[observability]
[observability.grafana]

View File

@@ -154,6 +154,11 @@ export default function Files() {
const response = await nhost.storage.uploadFiles({
"bucket-id": "personal",
"file[]": [file as File],
"metadata[]": [
{
metadata: { key1: "value1" },
},
],
});
// Get the processed file data

View File

@@ -467,7 +467,12 @@ export default function Todos() {
)}
{showAddForm && (
<View style={[commonStyles.card, { marginHorizontal: 16, width: undefined }]}>
<View
style={[
commonStyles.card,
{ marginHorizontal: 16, width: undefined },
]}
>
<Text style={commonStyles.cardTitle}>Add New Todo</Text>
<View style={commonStyles.formFields}>
<View style={commonStyles.fieldGroup}>

File diff suppressed because it is too large Load Diff

View File

@@ -17,9 +17,11 @@
"expo-crypto": "14",
"expo-document-picker": "13",
"expo-file-system": "18",
"expo-linking": "^8.0.8",
"expo-router": "~6",
"expo-sharing": "13",
"expo-status-bar": "~3.0.8",
"metro-minify-terser": "^0.83.3",
"react": "19.1.0",
"react-native": "0.81.4"
},
@@ -27,5 +29,10 @@
"@types/react": "~19.1.0",
"typescript": "~5.9.2"
},
"private": true
"private": true,
"pnpm": {
"overrides": {
"js-yaml@<=4.1.0": ">=4.1.1"
}
}
}

View File

@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
js-yaml@<=4.1.0: '>=4.1.1'
importers:
.:
@@ -32,6 +35,9 @@ importers:
expo-file-system:
specifier: '18'
version: 18.1.11(expo@54.0.9)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0))
expo-linking:
specifier: ^8.0.8
version: 8.0.8(expo@54.0.9)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0))(react@19.1.0)
expo-router:
specifier: ~6
version: 6.0.7(@expo/metro-runtime@6.1.2)(@types/react@19.1.13)(expo-constants@18.0.9)(expo-linking@8.0.8)(expo@54.0.9)(react-dom@19.1.1(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0))(react@19.1.0)
@@ -41,6 +47,9 @@ importers:
expo-status-bar:
specifier: ~3.0.8
version: 3.0.8(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0))(react@19.1.0)
metro-minify-terser:
specifier: ^0.83.3
version: 0.83.3
react:
specifier: 19.1.0
version: 19.1.0
@@ -1221,9 +1230,6 @@ packages:
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@@ -1617,11 +1623,6 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
@@ -2021,12 +2022,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
hasBin: true
js-yaml@4.1.0:
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsc-safe-url@0.2.4:
@@ -2232,6 +2229,10 @@ packages:
resolution: {integrity: sha512-zvIxnh7U0JQ7vT4quasKsijId3dOAWgq+ip2jF/8TMrPUqQabGrs04L2dd0haQJ+PA+d4VvK/bPOY8X/vL2PWw==}
engines: {node: '>=20.19.4'}
metro-minify-terser@0.83.3:
resolution: {integrity: sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==}
engines: {node: '>=20.19.4'}
metro-resolver@0.83.1:
resolution: {integrity: sha512-t8j46kiILAqqFS5RNa+xpQyVjULxRxlvMidqUswPEk5nQVNdlJslqizDm/Et3v/JKwOtQGkYAQCHxP1zGStR/g==}
engines: {node: '>=20.19.4'}
@@ -2827,9 +2828,6 @@ packages:
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
engines: {node: '>=6'}
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
stack-utils@2.0.6:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
@@ -4121,7 +4119,7 @@ snapshots:
'@babel/code-frame': 7.10.4
chalk: 4.1.2
find-up: 5.0.0
js-yaml: 4.1.0
js-yaml: 4.1.1
'@isaacs/cliui@8.0.2':
dependencies:
@@ -4143,7 +4141,7 @@ snapshots:
camelcase: 5.3.1
find-up: 4.1.0
get-package-type: 0.1.0
js-yaml: 3.14.1
js-yaml: 4.1.1
resolve-from: 5.0.0
'@istanbuljs/schema@0.1.3': {}
@@ -4726,10 +4724,6 @@ snapshots:
arg@5.0.2: {}
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3
argparse@2.0.1: {}
aria-hidden@1.2.6:
@@ -5061,7 +5055,7 @@ snapshots:
dependencies:
import-fresh: 2.0.0
is-directory: 0.3.1
js-yaml: 3.14.1
js-yaml: 4.1.1
parse-json: 4.0.0
cross-spawn@7.0.6:
@@ -5146,8 +5140,6 @@ snapshots:
escape-string-regexp@4.0.0: {}
esprima@4.0.1: {}
etag@1.8.1: {}
event-target-shim@5.0.1: {}
@@ -5597,12 +5589,7 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.14.1:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
js-yaml@4.1.0:
js-yaml@4.1.1:
dependencies:
argparse: 2.0.1
@@ -5840,6 +5827,11 @@ snapshots:
flow-enums-runtime: 0.0.6
terser: 5.44.0
metro-minify-terser@0.83.3:
dependencies:
flow-enums-runtime: 0.0.6
terser: 5.44.0
metro-resolver@0.83.1:
dependencies:
flow-enums-runtime: 0.0.6
@@ -6588,8 +6580,6 @@ snapshots:
split-on-first@1.1.0: {}
sprintf-js@1.0.3: {}
stack-utils@2.0.6:
dependencies:
escape-string-regexp: 2.0.0

View File

@@ -27,5 +27,10 @@
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^7.0.8"
},
"pnpm": {
"overrides": {
"cookie@<0.7.0": ">=0.7.0"
}
}
}

View File

@@ -4,6 +4,9 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
cookie@<0.7.0: '>=0.7.0'
importers:
.:
@@ -384,9 +387,9 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
cookie@0.6.0:
resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
engines: {node: '>= 0.6'}
cookie@1.0.2:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
@@ -750,7 +753,7 @@ snapshots:
'@sveltejs/vite-plugin-svelte': 6.2.0(svelte@5.38.10)(vite@7.1.11)
'@types/cookie': 0.6.0
acorn: 8.15.0
cookie: 0.6.0
cookie: 1.0.2
devalue: 5.3.2
esm-env: 1.2.2
kleur: 4.1.5
@@ -799,7 +802,7 @@ snapshots:
clsx@2.1.1: {}
cookie@0.6.0: {}
cookie@1.0.2: {}
debug@4.4.3:
dependencies:

View File

@@ -125,7 +125,8 @@
"form-data@<2.5.4": ">=2.5.4",
"tmp@<=0.2.3": ">=0.2.4",
"devalue@<5.3.2": ">=5.3.2",
"axios@<1.12.0": ">=1.12.0"
"axios@<1.12.0": ">=1.12.0",
"js-yaml@<=4.1.0": ">=4.1.1"
}
}
}

View File

@@ -121,7 +121,8 @@
},
"pnpm": {
"overrides": {
"rollup@<2.79.2": ">=2.79.2"
"rollup@<2.79.2": ">=2.79.2",
"js-yaml@<=4.1.0": ">=4.1.1"
}
}
}

View File

@@ -6,6 +6,7 @@ settings:
overrides:
rollup@<2.79.2: '>=2.79.2'
js-yaml@<=4.1.0: '>=4.1.1'
importers:
@@ -531,8 +532,8 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
argparse@1.0.10:
resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
@@ -711,11 +712,6 @@ packages:
resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
engines: {node: '>=8'}
esprima@4.0.1:
resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==}
engines: {node: '>=4'}
hasBin: true
estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
@@ -1013,8 +1009,8 @@ packages:
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
js-yaml@3.14.1:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
js-yaml@4.1.1:
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
hasBin: true
jsesc@3.1.0:
@@ -1250,9 +1246,6 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
sprintf-js@1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
stack-utils@2.0.6:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
@@ -1614,7 +1607,7 @@ snapshots:
camelcase: 5.3.1
find-up: 4.1.0
get-package-type: 0.1.0
js-yaml: 3.14.1
js-yaml: 4.1.1
resolve-from: 5.0.0
'@istanbuljs/schema@0.1.3': {}
@@ -1973,9 +1966,7 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.1
argparse@1.0.10:
dependencies:
sprintf-js: 1.0.3
argparse@2.0.1: {}
async@3.2.6: {}
@@ -2158,8 +2149,6 @@ snapshots:
escape-string-regexp@2.0.0: {}
esprima@4.0.1: {}
estree-walker@2.0.2: {}
execa@5.1.1:
@@ -2635,10 +2624,9 @@ snapshots:
js-tokens@4.0.0: {}
js-yaml@3.14.1:
js-yaml@4.1.1:
dependencies:
argparse: 1.0.10
esprima: 4.0.1
argparse: 2.0.1
jsesc@3.1.0: {}
@@ -2849,8 +2837,6 @@ snapshots:
source-map@0.6.1: {}
sprintf-js@1.0.3: {}
stack-utils@2.0.6:
dependencies:
escape-string-regexp: 2.0.0

View File

@@ -632,16 +632,27 @@ export const createAPIClient = (
): Promise<FetchResponse<UploadFilesResponse201>> => {
const url = `${baseURL}/files`;
const formData = new FormData();
const isReactNative =
typeof navigator !== "undefined" &&
(navigator as { product?: string }).product === "ReactNative";
if (body["bucket-id"] !== undefined) {
formData.append("bucket-id", body["bucket-id"]);
}
if (body["metadata[]"] !== undefined) {
body["metadata[]"].forEach((value) => {
formData.append(
"metadata[]",
new Blob([JSON.stringify(value)], { type: "application/json" }),
"",
);
if (isReactNative) {
formData.append("metadata[]", {
string: JSON.stringify(value),
type: "application/json",
name: "",
} as unknown as Blob);
} else {
formData.append(
"metadata[]",
new Blob([JSON.stringify(value)], { type: "application/json" }),
"",
);
}
});
}
if (body["file[]"] !== undefined) {
@@ -799,14 +810,25 @@ export const createAPIClient = (
): Promise<FetchResponse<FileMetadata>> => {
const url = `${baseURL}/files/${id}`;
const formData = new FormData();
const isReactNative =
typeof navigator !== "undefined" &&
(navigator as { product?: string }).product === "ReactNative";
if (body["metadata"] !== undefined) {
formData.append(
"metadata",
new Blob([JSON.stringify(body["metadata"])], {
if (isReactNative) {
formData.append("metadata", {
string: JSON.stringify(body["metadata"]),
type: "application/json",
}),
"",
);
name: "",
} as unknown as Blob);
} else {
formData.append(
"metadata",
new Blob([JSON.stringify(body["metadata"])], {
type: "application/json",
}),
"",
);
}
}
if (body["file"] !== undefined) {
formData.append("file", body["file"]);

1
pnpm-lock.yaml generated
View File

@@ -90,6 +90,7 @@ overrides:
tmp@<=0.2.3: '>=0.2.4'
devalue@<5.3.2: '>=5.3.2'
axios@<1.12.0: '>=1.12.0'
js-yaml@<=4.1.0: '>=4.1.1'
importers:

View File

@@ -767,16 +767,30 @@ export const createAPIClient = (
): Promise<FetchResponse<UploadFilesResponse201>> => {
const url = `${ baseURL }/files/`;
const formData = new FormData();
const isReactNative =
typeof navigator !== "undefined" &&
(navigator as { product?: string }).product === "ReactNative";
if (body["bucket-id"] !== undefined) {
formData.append("bucket-id", body["bucket-id"]);
}
if (body["metadata[]"] !== undefined) {
body["metadata[]"].forEach((value) => {
formData.append(
if (isReactNative) {
formData.append(
"metadata[]",
{
string: JSON.stringify(value),
type: "application/json",
name: "",
} as unknown as Blob,
);
} else {
formData.append(
"metadata[]",
new Blob([JSON.stringify(value)], { type: "application/json" }),
"",
)
new Blob([JSON.stringify(value)], { type: "application/json" }),
"",
);
}
}
);
}
@@ -912,12 +926,26 @@ export const createAPIClient = (
): Promise<FetchResponse<FileMetadata>> => {
const url = `${ baseURL }/files/${id}`;
const formData = new FormData();
const isReactNative =
typeof navigator !== "undefined" &&
(navigator as { product?: string }).product === "ReactNative";
if (body["metadata"] !== undefined) {
formData.append(
if (isReactNative) {
formData.append(
"metadata",
{
string: JSON.stringify(body["metadata"]),
type: "application/json",
name: "",
} as unknown as Blob,
);
} else {
formData.append(
"metadata",
new Blob([JSON.stringify(body["metadata"])], { type: "application/json" }),
"",
);
new Blob([JSON.stringify(body["metadata"])], { type: "application/json" }),
"",
);
}
}
if (body["file"] !== undefined) {
formData.append("file", body["file"]);

View File

@@ -126,6 +126,9 @@ export const createAPIClient = (
});
{{- else if .RequestFormData }}
const formData = new FormData();
const isReactNative =
typeof navigator !== "undefined" &&
(navigator as { product?: string }).product === "ReactNative";
{{- range .RequestFormData.Properties }}
{{- if eq .Type.Kind "scalar" }}
@@ -138,11 +141,22 @@ export const createAPIClient = (
{{- if eq .Type.Item.Kind "scalar" }}
formData.append("{{ .Name }}", value)
{{- else if eq .Type.Item.Kind "object" }}
formData.append(
if (isReactNative) {
formData.append(
"{{ .Name }}",
{
string: JSON.stringify(value),
type: "application/json",
name: "",
} as unknown as Blob,
);
} else {
formData.append(
"{{ .Name }}",
new Blob([JSON.stringify(value)], { type: "application/json" }),
"",
)
new Blob([JSON.stringify(value)], { type: "application/json" }),
"",
);
}
{{- else }}
TODO {{ .Type.Kind }} {{ .Type.Schema.Schema.Type }}
{{- end }}
@@ -151,11 +165,22 @@ export const createAPIClient = (
}
{{- else if eq .Type.Kind "object" }}
if (body["{{ .Name }}"] !== undefined) {
formData.append(
if (isReactNative) {
formData.append(
"{{ .Name }}",
{
string: JSON.stringify(body["{{ .Name }}"]),
type: "application/json",
name: "",
} as unknown as Blob,
);
} else {
formData.append(
"{{ .Name }}",
new Blob([JSON.stringify(body["{{ .Name }}"])], { type: "application/json" }),
"",
);
new Blob([JSON.stringify(body["{{ .Name }}"])], { type: "application/json" }),
"",
);
}
}
{{- else }}
TODO {{ .Type.Kind }} {{ .Type.Schema.Schema.Type }}