Compare commits

...

12 Commits

235 changed files with 51862 additions and 171677 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

@@ -250,7 +250,7 @@ func traefik(subdomain, projectName string, port uint, dotnhostfolder string) (*
}
return &Service{
Image: "traefik:v3.1",
Image: "traefik:v3.6",
DependsOn: nil,
EntryPoint: nil,
Command: []string{

View File

@@ -4191,32 +4191,34 @@ type Plans struct {
// An array relationship
Organizations []*Organizations `json:"organizations"`
Price int64 `json:"price"`
SLALevel SLALevelEnum `json:"slaLevel"`
Sort int64 `json:"sort"`
UpatedAt time.Time `json:"upatedAt"`
}
// Boolean expression to filter rows from the table "plans". All fields are combined with a logical 'AND'.
type PlansBoolExp struct {
And []*PlansBoolExp `json:"_and,omitempty"`
Not *PlansBoolExp `json:"_not,omitempty"`
Or []*PlansBoolExp `json:"_or,omitempty"`
Apps *AppsBoolExp `json:"apps,omitempty"`
CreatedAt *TimestamptzComparisonExp `json:"createdAt,omitempty"`
Deprecated *BooleanComparisonExp `json:"deprecated,omitempty"`
FeatureBackupEnabled *BooleanComparisonExp `json:"featureBackupEnabled,omitempty"`
FeatureCustomDomainsEnabled *BooleanComparisonExp `json:"featureCustomDomainsEnabled,omitempty"`
FeatureCustomEmailTemplatesEnabled *BooleanComparisonExp `json:"featureCustomEmailTemplatesEnabled,omitempty"`
FeatureMaxDbSize *IntComparisonExp `json:"featureMaxDbSize,omitempty"`
ID *UUIDComparisonExp `json:"id,omitempty"`
Individual *BooleanComparisonExp `json:"individual,omitempty"`
IsDefault *BooleanComparisonExp `json:"isDefault,omitempty"`
IsFree *BooleanComparisonExp `json:"isFree,omitempty"`
IsPublic *BooleanComparisonExp `json:"isPublic,omitempty"`
Name *StringComparisonExp `json:"name,omitempty"`
Organizations *OrganizationsBoolExp `json:"organizations,omitempty"`
Price *IntComparisonExp `json:"price,omitempty"`
Sort *IntComparisonExp `json:"sort,omitempty"`
UpatedAt *TimestamptzComparisonExp `json:"upatedAt,omitempty"`
And []*PlansBoolExp `json:"_and,omitempty"`
Not *PlansBoolExp `json:"_not,omitempty"`
Or []*PlansBoolExp `json:"_or,omitempty"`
Apps *AppsBoolExp `json:"apps,omitempty"`
CreatedAt *TimestamptzComparisonExp `json:"createdAt,omitempty"`
Deprecated *BooleanComparisonExp `json:"deprecated,omitempty"`
FeatureBackupEnabled *BooleanComparisonExp `json:"featureBackupEnabled,omitempty"`
FeatureCustomDomainsEnabled *BooleanComparisonExp `json:"featureCustomDomainsEnabled,omitempty"`
FeatureCustomEmailTemplatesEnabled *BooleanComparisonExp `json:"featureCustomEmailTemplatesEnabled,omitempty"`
FeatureMaxDbSize *IntComparisonExp `json:"featureMaxDbSize,omitempty"`
ID *UUIDComparisonExp `json:"id,omitempty"`
Individual *BooleanComparisonExp `json:"individual,omitempty"`
IsDefault *BooleanComparisonExp `json:"isDefault,omitempty"`
IsFree *BooleanComparisonExp `json:"isFree,omitempty"`
IsPublic *BooleanComparisonExp `json:"isPublic,omitempty"`
Name *StringComparisonExp `json:"name,omitempty"`
Organizations *OrganizationsBoolExp `json:"organizations,omitempty"`
Price *IntComparisonExp `json:"price,omitempty"`
SLALevel *SLALevelEnumComparisonExp `json:"slaLevel,omitempty"`
Sort *IntComparisonExp `json:"sort,omitempty"`
UpatedAt *TimestamptzComparisonExp `json:"upatedAt,omitempty"`
}
// Ordering options when selecting data from "plans".
@@ -4236,6 +4238,7 @@ type PlansOrderBy struct {
Name *OrderBy `json:"name,omitempty"`
OrganizationsAggregate *OrganizationsAggregateOrderBy `json:"organizations_aggregate,omitempty"`
Price *OrderBy `json:"price,omitempty"`
SLALevel *OrderBy `json:"slaLevel,omitempty"`
Sort *OrderBy `json:"sort,omitempty"`
UpatedAt *OrderBy `json:"upatedAt,omitempty"`
}
@@ -4250,21 +4253,22 @@ type PlansStreamCursorInput struct {
// Initial value of the column from where the streaming should start
type PlansStreamCursorValueInput struct {
CreatedAt *time.Time `json:"createdAt,omitempty"`
Deprecated *bool `json:"deprecated,omitempty"`
FeatureBackupEnabled *bool `json:"featureBackupEnabled,omitempty"`
FeatureCustomDomainsEnabled *bool `json:"featureCustomDomainsEnabled,omitempty"`
FeatureCustomEmailTemplatesEnabled *bool `json:"featureCustomEmailTemplatesEnabled,omitempty"`
FeatureMaxDbSize *int64 `json:"featureMaxDbSize,omitempty"`
ID *string `json:"id,omitempty"`
Individual *bool `json:"individual,omitempty"`
IsDefault *bool `json:"isDefault,omitempty"`
IsFree *bool `json:"isFree,omitempty"`
IsPublic *bool `json:"isPublic,omitempty"`
Name *string `json:"name,omitempty"`
Price *int64 `json:"price,omitempty"`
Sort *int64 `json:"sort,omitempty"`
UpatedAt *time.Time `json:"upatedAt,omitempty"`
CreatedAt *time.Time `json:"createdAt,omitempty"`
Deprecated *bool `json:"deprecated,omitempty"`
FeatureBackupEnabled *bool `json:"featureBackupEnabled,omitempty"`
FeatureCustomDomainsEnabled *bool `json:"featureCustomDomainsEnabled,omitempty"`
FeatureCustomEmailTemplatesEnabled *bool `json:"featureCustomEmailTemplatesEnabled,omitempty"`
FeatureMaxDbSize *int64 `json:"featureMaxDbSize,omitempty"`
ID *string `json:"id,omitempty"`
Individual *bool `json:"individual,omitempty"`
IsDefault *bool `json:"isDefault,omitempty"`
IsFree *bool `json:"isFree,omitempty"`
IsPublic *bool `json:"isPublic,omitempty"`
Name *string `json:"name,omitempty"`
Price *int64 `json:"price,omitempty"`
SLALevel *SLALevelEnum `json:"slaLevel,omitempty"`
Sort *int64 `json:"sort,omitempty"`
UpatedAt *time.Time `json:"upatedAt,omitempty"`
}
type QueryRoot struct {
@@ -4694,6 +4698,15 @@ type RunServiceStreamCursorValueInput struct {
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
}
// Boolean expression to compare columns of type "sla_level_enum". All fields are combined with logical 'AND'.
type SLALevelEnumComparisonExp struct {
Eq *SLALevelEnum `json:"_eq,omitempty"`
In []SLALevelEnum `json:"_in,omitempty"`
IsNull *bool `json:"_is_null,omitempty"`
Neq *SLALevelEnum `json:"_neq,omitempty"`
Nin []SLALevelEnum `json:"_nin,omitempty"`
}
// Boolean expression to compare columns of type "software_type_enum". All fields are combined with logical 'AND'.
type SoftwareTypeEnumComparisonExp struct {
Eq *SoftwareTypeEnum `json:"_eq,omitempty"`
@@ -8542,6 +8555,8 @@ const (
// column name
PlansSelectColumnPrice PlansSelectColumn = "price"
// column name
PlansSelectColumnSLALevel PlansSelectColumn = "slaLevel"
// column name
PlansSelectColumnSort PlansSelectColumn = "sort"
// column name
PlansSelectColumnUpatedAt PlansSelectColumn = "upatedAt"
@@ -8561,13 +8576,14 @@ var AllPlansSelectColumn = []PlansSelectColumn{
PlansSelectColumnIsPublic,
PlansSelectColumnName,
PlansSelectColumnPrice,
PlansSelectColumnSLALevel,
PlansSelectColumnSort,
PlansSelectColumnUpatedAt,
}
func (e PlansSelectColumn) IsValid() bool {
switch e {
case PlansSelectColumnCreatedAt, PlansSelectColumnDeprecated, PlansSelectColumnFeatureBackupEnabled, PlansSelectColumnFeatureCustomDomainsEnabled, PlansSelectColumnFeatureCustomEmailTemplatesEnabled, PlansSelectColumnFeatureMaxDbSize, PlansSelectColumnID, PlansSelectColumnIndividual, PlansSelectColumnIsDefault, PlansSelectColumnIsFree, PlansSelectColumnIsPublic, PlansSelectColumnName, PlansSelectColumnPrice, PlansSelectColumnSort, PlansSelectColumnUpatedAt:
case PlansSelectColumnCreatedAt, PlansSelectColumnDeprecated, PlansSelectColumnFeatureBackupEnabled, PlansSelectColumnFeatureCustomDomainsEnabled, PlansSelectColumnFeatureCustomEmailTemplatesEnabled, PlansSelectColumnFeatureMaxDbSize, PlansSelectColumnID, PlansSelectColumnIndividual, PlansSelectColumnIsDefault, PlansSelectColumnIsFree, PlansSelectColumnIsPublic, PlansSelectColumnName, PlansSelectColumnPrice, PlansSelectColumnSLALevel, PlansSelectColumnSort, PlansSelectColumnUpatedAt:
return true
}
return false
@@ -8954,6 +8970,66 @@ func (e RunServiceSelectColumn) MarshalJSON() ([]byte, error) {
return buf.Bytes(), nil
}
type SLALevelEnum string
const (
// No SLA
SLALevelEnumNone SLALevelEnum = "none"
// Premium SLA
SLALevelEnumPremium SLALevelEnum = "premium"
// Standard SLA
SLALevelEnumStandard SLALevelEnum = "standard"
)
var AllSLALevelEnum = []SLALevelEnum{
SLALevelEnumNone,
SLALevelEnumPremium,
SLALevelEnumStandard,
}
func (e SLALevelEnum) IsValid() bool {
switch e {
case SLALevelEnumNone, SLALevelEnumPremium, SLALevelEnumStandard:
return true
}
return false
}
func (e SLALevelEnum) String() string {
return string(e)
}
func (e *SLALevelEnum) UnmarshalGQL(v any) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = SLALevelEnum(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid sla_level_enum", str)
}
return nil
}
func (e SLALevelEnum) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
func (e *SLALevelEnum) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
return e.UnmarshalGQL(s)
}
func (e SLALevelEnum) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
e.MarshalGQL(&buf)
return buf.Bytes(), nil
}
type SoftwareTypeEnum string
const (

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

@@ -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

@@ -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",
@@ -219,7 +220,8 @@
"pages": [
"/products/graphql/guides/react-apollo",
"/products/graphql/guides/react-query",
"/products/graphql/guides/react-urql"
"/products/graphql/guides/react-urql",
"/products/graphql/guides/codegen-nhost"
]
}
]

View File

@@ -1326,10 +1326,9 @@ After we complete the next tutorial on user authentication, you will be able to
1. **Server-Side Helpers**: Utilities for handling authentication in Next.js server components and middleware
2. **Middleware Route Protection**: Next.js middleware runs before any page renders, automatically redirecting unauthenticated users from protected routes and refreshing tokens
3. **AuthProvider**: Client-side provider that manages authentication state using Nhost's client with cookie-based storage for server/client synchronization
4. **Protected Pages**: Server components can assume authentication since middleware handles protection, focusing purely on rendering authenticated content
5. **Navigation**: Server-side navigation component that adapts its links based on authentication status
6. **Automatic Redirects**: All route protection and redirects are handled at the middleware level for optimal performance and security
3. **Protected Pages**: Server components can assume authentication since middleware handles protection, focusing purely on rendering authenticated content
4. **Navigation**: Server-side navigation component that adapts its links based on authentication status
5. **Automatic Redirects**: All route protection and redirects are handled at the middleware level for optimal performance and security
## Key Features Demonstrated

View File

@@ -27,6 +27,10 @@
"openapi-types": "*"
}
}
},
"overrides": {
"js-yaml@<=4.1.0": ">=4.1.1",
"glob@>=10.3.7 <=11.0.3": ">=11.1.0"
}
}
}

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

119
docs/pnpm-lock.yaml generated
View File

@@ -4,6 +4,10 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
js-yaml@<=4.1.0: '>=4.1.1'
glob@>=10.3.7 <=11.0.3: '>=11.1.0'
packageExtensionsChecksum: sha256-4+NJJHoeDEOtWI2UxgTNLimXyrOojBs00S85/9Babm0=
importers:
@@ -316,6 +320,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'}
@@ -420,10 +432,6 @@ packages:
'@openapi-contrib/openapi-schema-to-json-schema@3.2.0':
resolution: {integrity: sha512-Gj6C0JwCr8arj0sYuslWXUBSP/KnUlEGnPW4qxlXvAl543oaNQgMgIgkQUA6vs5BCCvwTEiL8m/wdWzfl4UvSw==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@puppeteer/browsers@2.3.0':
resolution: {integrity: sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==}
engines: {node: '>=18'}
@@ -936,9 +944,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==}
@@ -1717,8 +1722,9 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
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
globalthis@1.0.4:
@@ -2096,8 +2102,9 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jackspeak@4.1.1:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22}
jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
@@ -2106,12 +2113,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:
@@ -2187,8 +2190,9 @@ packages:
resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@11.2.2:
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
engines: {node: 20 || >=22}
lru-cache@7.18.3:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
@@ -2428,6 +2432,10 @@ packages:
resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
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==}
@@ -2641,9 +2649,9 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
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@0.1.12:
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
@@ -3126,9 +3134,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 +3604,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:
@@ -3849,6 +3854,12 @@ snapshots:
optionalDependencies:
'@types/node': 24.7.0
'@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
@@ -3937,7 +3948,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 +3990,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 +4105,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 +4142,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 +4172,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 +4207,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
@@ -4229,9 +4240,6 @@ snapshots:
dependencies:
fast-deep-equal: 3.1.3
'@pkgjs/parseargs@0.11.0':
optional: true
'@puppeteer/browsers@2.3.0':
dependencies:
debug: 4.4.3
@@ -4791,10 +4799,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 +5099,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
@@ -5655,14 +5659,14 @@ snapshots:
dependencies:
is-glob: 4.0.3
glob@10.4.5:
glob@12.0.0:
dependencies:
foreground-child: 3.3.1
jackspeak: 3.4.3
minimatch: 9.0.5
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
globalthis@1.0.4:
dependencies:
@@ -5703,7 +5707,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
@@ -6175,22 +6179,15 @@ snapshots:
isexe@2.0.0: {}
jackspeak@3.4.3:
jackspeak@4.1.1:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jiti@1.21.7: {}
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
@@ -6252,7 +6249,7 @@ snapshots:
lowercase-keys@3.0.0: {}
lru-cache@10.4.3: {}
lru-cache@11.2.2: {}
lru-cache@7.18.3: {}
@@ -6771,6 +6768,10 @@ snapshots:
mimic-response@4.0.0: {}
minimatch@10.1.1:
dependencies:
'@isaacs/brace-expansion': 5.0.0
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
@@ -7005,9 +7006,9 @@ snapshots:
path-parse@1.0.7: {}
path-scurry@1.11.1:
path-scurry@2.0.1:
dependencies:
lru-cache: 10.4.3
lru-cache: 11.2.2
minipass: 7.1.2
path-to-regexp@0.1.12: {}
@@ -7673,8 +7674,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
@@ -7762,7 +7761,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.7

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

@@ -0,0 +1,9 @@
---
title: "Codegen + Nhost"
description: "How to use The Guild's codegen with Nhost"
icon: U
---
You can use [The Guild's codegen](https://the-guild.dev/graphql/codegen) to generate types and document nodes for your GraphQL operations and use them directly with Nhost's GraphQL client.
You can find a working example with instructions in our [GitHub repository](https://github.com/nhost/nhost/blob/main/examples/guides/codegen-nhost/README.md).

View File

@@ -792,7 +792,7 @@ Maximum height to resize image to while maintaining aspect ratio. Only applies t
optional q: number;
```
Image quality (1-100). Only applies to JPEG, WebP and PNG files
Image quality (1-100). Only applies to JPEG, WebP, PNG and HEIC files
#### w?
@@ -842,7 +842,7 @@ Maximum height to resize image to while maintaining aspect ratio. Only applies t
optional q: number;
```
Image quality (1-100). Only applies to JPEG, WebP and PNG files
Image quality (1-100). Only applies to JPEG, WebP, PNG and HEIC files
#### w?
@@ -1068,7 +1068,7 @@ buildVersion: string
## OutputImageFormat
```ts
type OutputImageFormat = 'auto' | 'same' | 'jpeg' | 'webp' | 'png' | 'avif'
type OutputImageFormat = 'auto' | 'same' | 'jpeg' | 'webp' | 'png' | 'avif' | 'heic'
```
Output format for image files. Use 'auto' for content negotiation based on Accept header

View File

@@ -148,7 +148,7 @@ paths:
schema:
$ref: '#/components/schemas/RFC2822Date'
- name: q
description: "Image quality (1-100). Only applies to JPEG, WebP and PNG files"
description: "Image quality (1-100). Only applies to JPEG, WebP, PNG and HEIC files"
in: query
schema:
type: integer
@@ -332,7 +332,7 @@ paths:
schema:
$ref: '#/components/schemas/RFC2822Date'
- name: q
description: "Image quality (1-100). Only applies to JPEG, WebP and PNG files"
description: "Image quality (1-100). Only applies to JPEG, WebP, PNG and HEIC files"
in: query
schema:
type: integer
@@ -614,7 +614,7 @@ paths:
schema:
$ref: '#/components/schemas/RFC2822Date'
- name: q
description: "Image quality (1-100). Only applies to JPEG, WebP and PNG files"
description: "Image quality (1-100). Only applies to JPEG, WebP, PNG and HEIC files"
in: query
schema:
type: integer
@@ -1178,4 +1178,5 @@ components:
- webp
- png
- avif
- heic
example: same

View File

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

View File

@@ -4,6 +4,10 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
js-yaml@<=4.1.0: '>=4.1.1'
glob@>=10.3.7 <=11.0.3: '>=11.1.0'
importers:
.:
@@ -713,6 +717,14 @@ packages:
resolution: {integrity: sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw==}
hasBin: 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'}
@@ -773,10 +785,6 @@ packages:
'@jridgewell/trace-mapping@0.3.30':
resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
@@ -1064,9 +1072,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 +1448,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'}
@@ -1703,8 +1703,9 @@ packages:
resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==}
engines: {node: '>=6'}
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.2.3:
@@ -1832,8 +1833,9 @@ packages:
resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==}
engines: {node: '>=8'}
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jackspeak@4.1.1:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22}
jest-environment-node@29.7.0:
resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==}
@@ -1877,12 +1879,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:
@@ -2016,6 +2014,10 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
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==}
@@ -2118,6 +2120,10 @@ packages:
resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==}
engines: {node: '>=4'}
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==}
@@ -2281,9 +2287,9 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
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}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2623,9 +2629,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'}
@@ -3600,7 +3603,7 @@ snapshots:
env-editor: 0.4.2
freeport-async: 2.0.0
getenv: 2.0.0
glob: 10.4.5
glob: 12.0.0
lan-network: 0.1.7
minimatch: 9.0.5
node-forge: 1.3.1
@@ -3648,7 +3651,7 @@ snapshots:
chalk: 4.1.2
debug: 4.4.1
getenv: 2.0.0
glob: 10.4.5
glob: 12.0.0
resolve-from: 5.0.0
semver: 7.7.2
slash: 3.0.0
@@ -3668,7 +3671,7 @@ snapshots:
'@expo/json-file': 9.1.5
deepmerge: 4.3.1
getenv: 2.0.0
glob: 10.4.5
glob: 12.0.0
require-from-string: 2.0.2
resolve-from: 5.0.0
resolve-workspace-root: 2.0.0
@@ -3682,7 +3685,7 @@ snapshots:
dependencies:
'@expo/sudo-prompt': 9.3.2
debug: 3.2.7
glob: 10.4.5
glob: 12.0.0
transitivePeerDependencies:
- supports-color
@@ -3704,7 +3707,7 @@ snapshots:
debug: 4.4.1
find-up: 5.0.0
getenv: 2.0.0
glob: 10.4.5
glob: 12.0.0
ignore: 5.3.2
minimatch: 9.0.5
p-limit: 3.1.0
@@ -3745,7 +3748,7 @@ snapshots:
dotenv: 16.4.7
dotenv-expand: 11.0.7
getenv: 2.0.0
glob: 10.4.5
glob: 12.0.0
jsc-safe-url: 0.2.4
lightningcss: 1.27.0
minimatch: 9.0.5
@@ -3825,7 +3828,13 @@ 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/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0':
dependencies:
'@isaacs/balanced-match': 4.0.1
'@isaacs/cliui@8.0.2':
dependencies:
@@ -3847,7 +3856,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': {}
@@ -3924,9 +3933,6 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@pkgjs/parseargs@0.11.0':
optional: true
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.0.14)(react@19.0.0)':
dependencies:
react: 19.0.0
@@ -4296,10 +4302,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 +4617,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 +4700,6 @@ snapshots:
escape-string-regexp@4.0.0: {}
esprima@4.0.1: {}
etag@1.8.1: {}
event-target-shim@5.0.1: {}
@@ -4793,7 +4793,7 @@ snapshots:
chalk: 4.1.2
commander: 7.2.0
find-up: 5.0.0
glob: 10.4.5
glob: 12.0.0
require-from-string: 2.0.2
resolve-from: 5.0.0
@@ -4969,14 +4969,14 @@ snapshots:
getenv@2.0.0: {}
glob@10.4.5:
glob@12.0.0:
dependencies:
foreground-child: 3.3.1
jackspeak: 3.4.3
minimatch: 9.0.5
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.2.3:
dependencies:
@@ -5096,11 +5096,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
jackspeak@3.4.3:
jackspeak@4.1.1:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jest-environment-node@29.7.0:
dependencies:
@@ -5178,12 +5176,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
@@ -5281,6 +5274,8 @@ snapshots:
lru-cache@10.4.3: {}
lru-cache@11.2.2: {}
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -5491,6 +5486,10 @@ snapshots:
mimic-fn@1.2.0: {}
minimatch@10.1.1:
dependencies:
'@isaacs/brace-expansion': 5.0.0
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
@@ -5629,9 +5628,9 @@ snapshots:
path-parse@1.0.7: {}
path-scurry@1.11.1:
path-scurry@2.0.1:
dependencies:
lru-cache: 10.4.3
lru-cache: 11.2.2
minipass: 7.1.2
picocolors@1.1.1: {}
@@ -6011,8 +6010,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
@@ -6063,7 +6060,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.7

View File

@@ -0,0 +1,25 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.vite

View File

@@ -0,0 +1,443 @@
# GraphQL Code Generation with Nhost SDK
This guide demonstrates how to use GraphQL Code Generator with TypedDocumentNode to get full type safety when working with the Nhost SDK.
Note: While the project uses React to illustrate usage, the generated types and documents can be used in any JavaScript/TypeScript environment.
## Overview
The Nhost SDK's GraphQL client supports `TypedDocumentNode` from `@graphql-typed-document-node/core`, allowing you to use generated types and documents for type-safe GraphQL operations. This guide shows you how to set up GraphQL Code Generator to work seamlessly with Nhost.
## Setup
### 1. Install Dependencies
```bash
npm install @nhost/nhost-js graphql graphql-typed-document-node/core
# or
yarn add @nhost/nhost-js graphql graphql-typed-document-node/core
# or
pnpm add @nhost/nhost-js graphql graphql-typed-document-node/core
```
### 2. Install GraphQL CodeGen
Install the necessary code generation packages:
```bash
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset @graphql-codegen/schema-ast
# or
pnpm add -D @graphql-codegen/cli @graphql-codegen/client-preset @graphql-codegen/schema-ast
```
### 3. Configure GraphQL CodeGen
Create a `codegen.ts` file with the client preset configuration:
```typescript
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: [
{
"https://local.graphql.local.nhost.run/v1": {
headers: {
"x-hasura-admin-secret": "nhost-admin-secret",
},
},
},
],
documents: ["src/lib/graphql/**/*.graphql"],
ignoreNoDocuments: true,
generates: {
"./src/lib/graphql/__generated__/": {
preset: "client",
presetConfig: {
persistedDocuments: false,
},
plugins: [
{
"./add-query-source-plugin.cjs": {},
},
],
config: {
scalars: {
UUID: "string",
uuid: "string",
timestamptz: "string",
jsonb: "Record<string, any>",
bigint: "number",
bytea: "Buffer",
citext: "string",
},
useTypeImports: true,
},
},
"./schema.graphql": {
plugins: ["schema-ast"],
config: {
includeDirectives: true,
},
},
},
};
export default config;
```
### 4. Create the Custom Plugin
The Nhost SDK expects documents to have a `loc.source.body` property containing the query string. Create a custom plugin to add this:
**add-query-source-plugin.cjs:**
```javascript
// Custom GraphQL Codegen plugin to add loc.source.body to generated documents
// This allows the Nhost SDK to extract the query string without needing the graphql package
const { print } = require("graphql");
/**
* @type {import('@graphql-codegen/plugin-helpers').PluginFunction}
*/
const plugin = (_schema, documents, _config) => {
let output = "";
for (const doc of documents) {
if (!doc.document) continue;
for (const definition of doc.document.definitions) {
if (definition.kind === "OperationDefinition" && definition.name) {
const operationName = definition.name.value;
const documentName = `${operationName}Document`;
// Create a document with just this operation
const singleOpDocument = {
kind: "Document",
definitions: [definition],
};
// Use graphql print to convert AST to string
const source = print(singleOpDocument);
output += `
// Add query source to ${documentName}
if (${documentName}) {
Object.assign(${documentName}, {
loc: { source: { body: ${JSON.stringify(source)} } }
});
}
`;
}
}
}
return output;
};
module.exports = { plugin };
```
## Integration Guide
### 1. Create an Auth Provider
Create an authentication context to manage the Nhost client and user session:
```typescript
// src/lib/nhost/AuthProvider.tsx
import {
createContext,
useContext,
useEffect,
useState,
useMemo,
type ReactNode,
} from "react";
import { createClient, type NhostClient } from "@nhost/nhost-js";
import { type Session } from "@nhost/nhost-js/auth";
interface AuthContextType {
user: Session["user"] | null;
session: Session | null;
isAuthenticated: boolean;
isLoading: boolean;
nhost: NhostClient;
}
const AuthContext = createContext<AuthContextType | null>(null);
export const AuthProvider = ({ children }: { children: ReactNode }) => {
const [user, setUser] = useState<Session["user"] | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
// Create the nhost client
const nhost = useMemo(
() =>
createClient({
region: import.meta.env.VITE_NHOST_REGION || "local",
subdomain: import.meta.env.VITE_NHOST_SUBDOMAIN || "local",
}),
[],
);
useEffect(() => {
setIsLoading(true);
const currentSession = nhost.getUserSession();
setUser(currentSession?.user || null);
setSession(currentSession);
setIsAuthenticated(!!currentSession);
setIsLoading(false);
const unsubscribe = nhost.sessionStorage.onChange((currentSession) => {
setUser(currentSession?.user || null);
setSession(currentSession);
setIsAuthenticated(!!currentSession);
});
return () => {
unsubscribe();
};
}, [nhost]);
const value: AuthContextType = {
user,
session,
isAuthenticated,
isLoading,
nhost,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};
```
### 2. Set Up Your App Providers
Wrap your application with the Auth provider:
```tsx
// src/main.tsx
import React from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
import { AuthProvider } from "./lib/nhost/AuthProvider";
const Root = () => (
<React.StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>
);
const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Root element not found");
createRoot(rootElement).render(<Root />);
```
### 3. Define GraphQL Operations
Create GraphQL files with your queries and mutations:
```graphql
# src/lib/graphql/operations.graphql
query GetNinjaTurtlesWithComments {
ninjaTurtles {
id
name
description
createdAt
updatedAt
comments {
id
comment
createdAt
user {
id
displayName
email
}
}
}
}
mutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {
insertComment(object: { ninjaTurtleId: $ninjaTurtleId, comment: $comment }) {
id
comment
createdAt
ninjaTurtleId
}
}
```
### 4. Generate TypeScript Types
Run the code generator:
```bash
npx graphql-codegen
```
You can also add a script to your `package.json`:
```json
{
"scripts": {
"generate": "graphql-codegen --config codegen.ts"
}
}
```
Then run:
```bash
npm run generate
# or
pnpm generate
```
### 5. Use in Components
Use the generated types and documents with the Nhost SDK:
```tsx
// src/pages/Home.tsx
import { type JSX, useCallback, useEffect, useState } from "react";
import {
AddCommentDocument,
GetNinjaTurtlesWithCommentsDocument,
type GetNinjaTurtlesWithCommentsQuery,
} from "../lib/graphql/__generated__/graphql";
import { useAuth } from "../lib/nhost/AuthProvider";
export default function Home(): JSX.Element {
const { isLoading, nhost } = useAuth();
const [activeCommentId, setActiveCommentId] = useState<string | null>(null);
const [commentText, setCommentText] = useState("");
const [activeTabId, setActiveTabId] = useState<string | null>(null);
const [data, setData] = useState<GetNinjaTurtlesWithCommentsQuery | null>(
null,
);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
// Fetch ninja turtles data
const fetchNinjaTurtles = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await nhost.graphql.request(
GetNinjaTurtlesWithCommentsDocument,
{},
);
if (result.body.errors) {
throw new Error(result.body.errors[0]?.message);
}
setData(result.body.data ?? null);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}, [nhost.graphql]);
// Load data on mount
useEffect(() => {
if (!isLoading) {
fetchNinjaTurtles();
}
}, [isLoading, fetchNinjaTurtles]);
const addComment = async (ninjaTurtleId: string, comment: string) => {
try {
const result = await nhost.graphql.request(AddCommentDocument, {
ninjaTurtleId,
comment,
});
if (result.body.errors) {
throw new Error(result.body.errors[0]?.message);
}
// Clear form and refetch data
setCommentText("");
setActiveCommentId(null);
await fetchNinjaTurtles();
} catch (err) {
console.error("Error adding comment:", err);
}
};
// ... rest of component
}
```
## Key Points
### Type-Safe GraphQL Requests
The Nhost SDK's `graphql.request()` method has overloads that support `TypedDocumentNode`:
```typescript
// Type inference works automatically
const result = await nhost.graphql.request(
GetNinjaTurtlesWithCommentsDocument,
{}, // Variables are type-checked
);
// result.body.data is typed as GetNinjaTurtlesWithCommentsQuery | undefined
```
### How It Works
1. **GraphQL Code Generator** creates `TypedDocumentNode` types and documents using the client preset
2. **Custom Plugin** adds the `loc.source.body` property to each document at runtime
3. **Nhost SDK** detects the `TypedDocumentNode`, extracts the query string from `loc.source.body`, and executes the request
4. **TypeScript** infers response types automatically based on the document types
### Benefits
- ✅ Full type safety for queries, mutations, and variables
- ✅ Automatic type inference - no manual type annotations needed
- ✅ Type-checked variables prevent runtime errors
- ✅ IntelliSense support in your IDE
- ✅ Compile-time errors for invalid queries or mismatched types
## Troubleshooting
### "not a valid graphql query" Error
If you see this error, make sure:
1. The custom plugin (`add-query-source-plugin.cjs`) is in place
2. The plugin is configured in your `codegen.ts`
3. You've run `pnpm generate` after adding the plugin
### TypeScript Errors
If you get type errors:
1. Make sure you're not passing explicit generic type parameters to `nhost.graphql.request()`
2. Let TypeScript infer types from the document
3. Pass an empty object `{}` for queries without variables
## Additional Resources
- [GraphQL Code Generator Docs](https://the-guild.dev/graphql/codegen)
- [Nhost Documentation](https://docs.nhost.io)
- [TypedDocumentNode](https://github.com/dotansimha/graphql-typed-document-node)

View File

@@ -0,0 +1,44 @@
// Custom GraphQL Codegen plugin to add loc.source.body to generated documents
// This allows the Nhost SDK to extract the query string without needing the graphql package
const { print } = require("graphql");
/**
* @type {import('@graphql-codegen/plugin-helpers').PluginFunction}
*/
const plugin = (_schema, documents, _config) => {
let output = "";
for (const doc of documents) {
if (!doc.document) continue;
for (const definition of doc.document.definitions) {
if (definition.kind === "OperationDefinition" && definition.name) {
const operationName = definition.name.value;
const documentName = `${operationName}Document`;
// Create a document with just this operation
const singleOpDocument = {
kind: "Document",
definitions: [definition],
};
// Use graphql print to convert AST to string
const source = print(singleOpDocument);
output += `
// Add query source to ${documentName}
if (${documentName}) {
Object.assign(${documentName}, {
loc: { source: { body: ${JSON.stringify(source)} } }
});
}
`;
}
}
}
return output;
};
module.exports = { plugin };

View File

@@ -0,0 +1,7 @@
{
"root": false,
"extends": "//",
"linter": {
"includes": ["**", "!src/lib/graphql/__generated__/*.ts"]
}
}

View File

@@ -0,0 +1,27 @@
#!/bin/bash
set -e
echo "Running GraphQL code generator..."
pnpm graphql-codegen --config codegen.ts
GENERATED_TS_FILE="src/lib/graphql/__generated__/"
GENERATED_SCHEMA_FILE="schema.graphql"
if [ -d "$GENERATED_TS_FILE" ]; then
echo "Formatting $GENERATED_TS_FILE..."
biome check --write "$GENERATED_TS_FILE"
else
echo "Error: Generated TypeScript file not found at $GENERATED_TS_FILE"
exit 1
fi
if [ -f "$GENERATED_SCHEMA_FILE" ]; then
echo "Formatting $GENERATED_SCHEMA_FILE..."
biome check --write "$GENERATED_SCHEMA_FILE"
echo "Successfully formatted $GENERATED_SCHEMA_FILE"
else
echo "Warning: Generated schema file not found at $GENERATED_SCHEMA_FILE"
fi
echo "All tasks completed successfully."

View File

@@ -0,0 +1,48 @@
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: [
{
"https://local.graphql.local.nhost.run/v1": {
headers: {
"x-hasura-admin-secret": "nhost-admin-secret",
},
},
},
],
documents: ["src/lib/graphql/**/*.graphql"],
ignoreNoDocuments: true,
generates: {
"./src/lib/graphql/__generated__/": {
preset: "client",
presetConfig: {
persistedDocuments: false,
},
plugins: [
{
"./add-query-source-plugin.cjs": {},
},
],
config: {
scalars: {
UUID: "string",
uuid: "string",
timestamptz: "string",
jsonb: "Record<string, any>",
bigint: "number",
bytea: "Buffer",
citext: "string",
},
useTypeImports: true,
},
},
"./schema.graphql": {
plugins: ["schema-ast"],
config: {
includeDirectives: true,
},
},
},
};
export default config;

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,40 @@
{
"name": "guides/codegen-nhost",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"generate": "bash codegen-wrapper.sh",
"test": "pnpm test:typecheck && pnpm test:lint",
"test:typecheck": "tsc --noEmit",
"test:lint": "biome check",
"format": "biome format --write",
"preview": "vite preview"
},
"dependencies": {
"@graphql-typed-document-node/core": "^3.2.0",
"@nhost/nhost-js": "workspace:*",
"graphql": "^16.11.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0"
},
"devDependencies": {
"@graphql-codegen/cli": "^5.0.6",
"@graphql-codegen/client-preset": "^5.1.2",
"@graphql-codegen/schema-ast": "^4.1.0",
"@graphql-codegen/typescript": "^4.1.6",
"@graphql-codegen/typescript-operations": "^4.6.1",
"@types/node": "^22.15.17",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1"
},
"pnpm": {
"overrides": {
"js-yaml@<4.1.1": ">=4.1.1"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,56 @@
import type { JSX } from "react";
import {
createBrowserRouter,
createRoutesFromElements,
Navigate,
Outlet,
Route,
RouterProvider,
} from "react-router-dom";
import Navigation from "./components/Navigation";
import ProtectedRoute from "./components/ProtectedRoute";
import Home from "./pages/Home";
import Profile from "./pages/Profile";
import SignIn from "./pages/SignIn";
import SignUp from "./pages/SignUp";
// Root layout component to wrap all routes
const RootLayout = (): JSX.Element => {
return (
<div className="flex-col min-h-screen">
<Navigation />
<main className="max-w-2xl mx-auto p-6 w-full">
<Outlet />
</main>
<footer>
<p
className="text-sm text-center"
style={{ color: "var(--text-muted)" }}
>
© {new Date().getFullYear()} Nhost Demo
</p>
</footer>
</div>
);
};
// Create router with routes
const router = createBrowserRouter(
createRoutesFromElements(
<Route element={<RootLayout />}>
<Route path="signin" element={<SignIn />} />
<Route path="signup" element={<SignUp />} />
<Route element={<ProtectedRoute />}>
<Route path="home" element={<Home />} />
<Route path="profile" element={<Profile />} />
</Route>
<Route path="*" element={<Navigate to="/" />} />
</Route>,
),
);
const App = (): JSX.Element => {
return <RouterProvider router={router} />;
};
export default App;

View File

@@ -0,0 +1,85 @@
import type { JSX } from "react";
import { Link, useLocation } from "react-router-dom";
import { useAuth } from "../lib/nhost/AuthProvider";
export default function Navigation(): JSX.Element {
const { isAuthenticated, nhost, session } = useAuth();
const location = useLocation();
// Helper function to determine if a link is active
const isActive = (path: string): string => {
return location.pathname === path ? "active" : "";
};
return (
<nav className="navbar">
<div className="navbar-container">
<div className="flex items-center">
<span className="navbar-brand">Nhost Demo</span>
<div className="navbar-links">
{isAuthenticated ? (
<>
<Link to="/home" className={`nav-link ${isActive("/home")}`}>
Home
</Link>
<Link
to="/profile"
className={`nav-link ${isActive("/profile")}`}
>
Profile
</Link>
</>
) : (
<>
<Link
to="/signin"
className={`nav-link ${isActive("/signin")}`}
>
Sign In
</Link>
<Link
to="/signup"
className={`nav-link ${isActive("/signup")}`}
>
Sign Up
</Link>
</>
)}
</div>
</div>
{isAuthenticated && (
<div>
<button
type="button"
onClick={async () => {
if (session) {
await nhost.auth.signOut({
refreshToken: session.refreshToken,
});
}
}}
className="icon-button"
title="Sign Out"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Sign Out"
role="img"
>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>
</button>
</div>
)}
</div>
</nav>
);
}

View File

@@ -0,0 +1,26 @@
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "../lib/nhost/AuthProvider";
interface ProtectedRouteProps {
redirectTo?: string;
}
export default function ProtectedRoute({
redirectTo = "/signin",
}: ProtectedRouteProps) {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return (
<div className="loading-container">
<p>Loading...</p>
</div>
);
}
if (!isAuthenticated) {
return <Navigate to={redirectTo} />;
}
return <Outlet />;
}

View File

@@ -0,0 +1,552 @@
/* Base styles */
:root {
--background: #030712;
--foreground: #ffffff;
--card-bg: #111827;
--card-border: #1f2937;
--primary: #6366f1;
--primary-hover: #4f46e5;
--secondary: #10b981;
--secondary-hover: #059669;
--accent: #8b5cf6;
--accent-hover: #7c3aed;
--success: #22c55e;
--error: #ef4444;
--text-primary: #f9fafb;
--text-secondary: #d1d5db;
--text-muted: #9ca3af;
--border-color: rgba(31, 41, 55, 0.7);
--font-geist-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: var(--background);
color: var(--foreground);
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
min-height: 100vh;
}
/* Layout */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.min-h-screen {
min-height: 100vh;
}
.w-full {
width: 100%;
}
.max-w-2xl {
max-width: 42rem;
}
.mx-auto {
margin-left: auto;
margin-right: auto;
}
.p-6 {
padding: 1.5rem;
}
.p-8 {
padding: 2rem;
}
.py-5 {
padding-top: 1.25rem;
padding-bottom: 1.25rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mt-4 {
margin-top: 1rem;
}
.mr-8 {
margin-right: 2rem;
}
.ml-2 {
margin-left: 0.5rem;
}
.space-y-5 > * + * {
margin-top: 1.25rem;
}
.space-x-4 > * + * {
margin-left: 1rem;
}
/* Typography */
h1,
h2,
h3 {
font-weight: bold;
line-height: 1.2;
}
.text-3xl {
font-size: 1.875rem;
}
.text-2xl {
font-size: 1.5rem;
}
.text-xl {
font-size: 1.25rem;
}
.text-lg {
font-size: 1.125rem;
}
.text-sm {
font-size: 0.875rem;
}
.text-xs {
font-size: 0.75rem;
}
.font-bold {
font-weight: 700;
}
.font-semibold {
font-weight: 600;
}
.font-medium {
font-weight: 500;
}
.text-center {
text-align: center;
}
.gradient-text {
background: linear-gradient(to right, var(--primary), var(--accent));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* Components */
.glass-card {
background: rgba(17, 24, 39, 0.7);
backdrop-filter: blur(8px);
border: 1px solid var(--border-color);
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
}
.btn {
display: inline-block;
padding: 0.625rem 1rem;
font-weight: 500;
border-radius: 0.375rem;
transition: all 0.2s ease;
cursor: pointer;
text-align: center;
border: none;
}
.btn-primary {
background-color: var(--primary);
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: var(--primary-hover);
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-secondary {
background-color: var(--secondary);
color: white;
}
.btn-secondary:hover:not(:disabled) {
background-color: var(--secondary-hover);
}
.nav-link {
display: inline-block;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.2s ease;
color: var(--text-secondary);
text-decoration: none;
}
.nav-link:hover {
color: white;
background-color: rgba(31, 41, 55, 0.7);
}
.nav-link.active {
background-color: var(--primary);
color: white;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
}
input,
textarea,
select {
width: 100%;
padding: 0.625rem 0.75rem;
background-color: rgba(31, 41, 55, 0.8);
border: 1px solid var(--border-color);
color: white;
border-radius: 0.375rem;
transition: all 0.2s;
}
input:focus,
textarea:focus,
select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
}
label {
display: block;
margin-bottom: 0.25rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
}
.alert {
padding: 0.75rem;
border-radius: 0.375rem;
margin-bottom: 1rem;
}
.alert-error {
background-color: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.5);
color: white;
}
.alert-success {
background-color: rgba(34, 197, 94, 0.2);
border: 1px solid rgba(34, 197, 94, 0.5);
color: white;
}
/* Navigation */
.navbar {
position: sticky;
top: 0;
z-index: 10;
background-color: rgba(17, 24, 39, 0.8);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--border-color);
padding: 1rem 0;
margin-bottom: 2rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.navbar-container {
display: flex;
justify-content: space-between;
align-items: center;
max-width: 42rem;
margin: 0 auto;
padding: 0 1.5rem;
}
.navbar-brand {
color: var(--primary);
font-weight: bold;
font-size: 1.125rem;
margin-right: 2rem;
}
.navbar-links {
display: flex;
gap: 1rem;
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
}
th {
text-align: left;
padding: 0.75rem 1rem;
font-size: 0.75rem;
text-transform: uppercase;
color: var(--text-secondary);
border-bottom: 1px solid var(--border-color);
}
td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border-color);
font-size: 0.875rem;
}
tr:hover {
background-color: rgba(31, 41, 55, 0.3);
}
/* File upload styles */
.file-upload {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 1.5rem;
border: 2px dashed rgba(99, 102, 241, 0.3);
border-radius: 0.5rem;
background-color: rgba(31, 41, 55, 0.3);
cursor: pointer;
transition: all 0.2s;
}
.file-upload:hover {
border-color: var(--primary);
}
/* Footer */
footer {
padding: 1.25rem 0;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: center;
align-items: center;
}
/* Link styles */
a {
color: var(--primary);
text-decoration: none;
transition: color 0.2s;
}
a:hover {
color: var(--primary-hover);
text-decoration: underline;
}
/* Loading state */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 50vh;
}
/* Code blocks */
pre {
background-color: rgba(31, 41, 55, 0.8);
padding: 1rem;
border-radius: 0.375rem;
overflow: auto;
font-family: monospace;
border: 1px solid var(--border-color);
font-size: 0.875rem;
color: var(--text-secondary);
}
/* Profile data */
.profile-item {
padding-bottom: 0.75rem;
margin-bottom: 0.75rem;
border-bottom: 1px solid var(--border-color);
}
.profile-item strong {
color: var(--text-secondary);
}
.action-link {
color: var(--primary);
font-weight: 500;
margin-right: 0.75rem;
cursor: pointer;
}
.action-link:hover {
color: var(--primary-hover);
text-decoration: underline;
}
.action-link-danger {
color: var(--error);
}
.action-link-danger:hover {
color: #f05252;
}
/* Icon button */
.icon-button {
background-color: transparent;
color: var(--primary);
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.icon-button:hover {
background-color: rgba(99, 102, 241, 0.1);
color: var(--primary-hover);
}
.icon-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.icon-button svg {
width: 20px;
height: 20px;
}
/* Table action icons */
.table-actions {
display: flex;
gap: 8px;
}
.action-icon {
background-color: transparent;
border: none;
width: 32px;
height: 32px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
padding: 0;
}
.action-icon svg {
width: 18px;
height: 18px;
}
.action-icon:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.action-icon-view {
color: var(--primary);
}
.action-icon-view:hover:not(:disabled) {
background-color: rgba(99, 102, 241, 0.1);
color: var(--primary-hover);
}
.action-icon-delete {
color: var(--error);
}
.action-icon-delete:hover:not(:disabled) {
background-color: rgba(239, 68, 68, 0.1);
color: #f05252;
}
/* Tab styles */
.tabs-container {
display: flex;
border-radius: 0.5rem;
overflow: hidden;
margin-bottom: 1.5rem;
border: 1px solid var(--border-color);
}
.tab-button {
flex: 1;
padding: 0.75rem 1rem;
font-weight: 500;
transition: all 0.2s ease;
background-color: rgba(31, 41, 55, 0.5);
color: var(--text-secondary);
}
.tab-button:hover:not(.tab-active) {
background-color: rgba(31, 41, 55, 0.8);
color: var(--text-primary);
}
.tab-button.tab-active {
background-color: var(--primary);
color: white;
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.3);
}
.tab-button:first-child {
border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
}
.tab-button:last-child {
border-top-right-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
.tab-content {
margin-top: 1.5rem;
}

View File

@@ -0,0 +1,110 @@
/* eslint-disable */
import type {
DocumentTypeDecoration,
ResultOf,
TypedDocumentNode,
} from "@graphql-typed-document-node/core";
import type { FragmentDefinitionNode } from "graphql";
import type { Incremental } from "./graphql";
export type FragmentType<
TDocumentType extends DocumentTypeDecoration<any, any>,
> = TDocumentType extends DocumentTypeDecoration<infer TType, any>
? [TType] extends [{ " $fragmentName"?: infer TKey }]
? TKey extends string
? { " $fragmentRefs"?: { [key in TKey]: TType } }
: never
: never
: never;
// return non-nullable if `fragmentType` is non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>,
): TType;
// return nullable if `fragmentType` is undefined
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | undefined,
): TType | undefined;
// return nullable if `fragmentType` is nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null,
): TType | null;
// return nullable if `fragmentType` is nullable or undefined
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType:
| FragmentType<DocumentTypeDecoration<TType, any>>
| null
| undefined,
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>>,
): Array<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType:
| Array<FragmentType<DocumentTypeDecoration<TType, any>>>
| null
| undefined,
): Array<TType> | null | undefined;
// return readonly array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>,
): ReadonlyArray<TType>;
// return readonly array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType:
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
| null
| undefined,
): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType:
| FragmentType<DocumentTypeDecoration<TType, any>>
| Array<FragmentType<DocumentTypeDecoration<TType, any>>>
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
| null
| undefined,
): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}
export function makeFragmentData<
F extends DocumentTypeDecoration<any, any>,
FT extends ResultOf<F>,
>(data: FT, _fragment: F): FragmentType<F> {
return data as FragmentType<F>;
}
export function isFragmentReady<TQuery, TFrag>(
queryNode: DocumentTypeDecoration<TQuery, any>,
fragmentNode: TypedDocumentNode<TFrag>,
data:
| FragmentType<TypedDocumentNode<Incremental<TFrag>, any>>
| null
| undefined,
): data is FragmentType<typeof fragmentNode> {
const deferredFields = (
queryNode as {
__meta__?: { deferredFields: Record<string, (keyof TFrag)[]> };
}
).__meta__?.deferredFields;
if (!deferredFields) return true;
const fragDef = fragmentNode.definitions[0] as
| FragmentDefinitionNode
| undefined;
const fragName = fragDef?.name?.value;
const fields = (fragName && deferredFields[fragName]) || [];
return fields.length > 0 && fields.every((field) => data && field in data);
}

View File

@@ -0,0 +1,51 @@
/* eslint-disable */
import type { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core";
import * as types from "./graphql";
/**
* Map of all GraphQL operations in the project.
*
* This map has several performance disadvantages:
* 1. It is not tree-shakeable, so it will include all operations in the project.
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel or swc plugin for production.
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
*/
type Documents = {
"query GetNinjaTurtlesWithComments {\n ninjaTurtles {\n id\n name\n description\n createdAt\n updatedAt\n comments {\n id\n comment\n createdAt\n user {\n id\n displayName\n email\n }\n }\n }\n}\n\nmutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {\n insertComment(object: {ninjaTurtleId: $ninjaTurtleId, comment: $comment}) {\n id\n comment\n createdAt\n ninjaTurtleId\n }\n}": typeof types.GetNinjaTurtlesWithCommentsDocument;
};
const documents: Documents = {
"query GetNinjaTurtlesWithComments {\n ninjaTurtles {\n id\n name\n description\n createdAt\n updatedAt\n comments {\n id\n comment\n createdAt\n user {\n id\n displayName\n email\n }\n }\n }\n}\n\nmutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {\n insertComment(object: {ninjaTurtleId: $ninjaTurtleId, comment: $comment}) {\n id\n comment\n createdAt\n ninjaTurtleId\n }\n}":
types.GetNinjaTurtlesWithCommentsDocument,
};
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*
*
* @example
* ```ts
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
* ```
*
* The query argument is unknown!
* Please regenerate the types.
*/
export function graphql(source: string): unknown;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(
source: "query GetNinjaTurtlesWithComments {\n ninjaTurtles {\n id\n name\n description\n createdAt\n updatedAt\n comments {\n id\n comment\n createdAt\n user {\n id\n displayName\n email\n }\n }\n }\n}\n\nmutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {\n insertComment(object: {ninjaTurtleId: $ninjaTurtleId, comment: $comment}) {\n id\n comment\n createdAt\n ninjaTurtleId\n }\n}",
): (typeof documents)["query GetNinjaTurtlesWithComments {\n ninjaTurtles {\n id\n name\n description\n createdAt\n updatedAt\n comments {\n id\n comment\n createdAt\n user {\n id\n displayName\n email\n }\n }\n }\n}\n\nmutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {\n insertComment(object: {ninjaTurtleId: $ninjaTurtleId, comment: $comment}) {\n id\n comment\n createdAt\n ninjaTurtleId\n }\n}"];
export function graphql(source: string) {
return (documents as any)[source] ?? {};
}
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> =
TDocumentNode extends DocumentNode<infer TType, any> ? TType : never;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
export * from "./fragment-masking";
export * from "./gql";

View File

@@ -0,0 +1,28 @@
query GetNinjaTurtlesWithComments {
ninjaTurtles {
id
name
description
createdAt
updatedAt
comments {
id
comment
createdAt
user {
id
displayName
email
}
}
}
}
mutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {
insertComment(object: { ninjaTurtleId: $ninjaTurtleId, comment: $comment }) {
id
comment
createdAt
ninjaTurtleId
}
}

View File

@@ -0,0 +1,175 @@
import { createClient, type NhostClient } from "@nhost/nhost-js";
import type { Session } from "@nhost/nhost-js/auth";
import {
createContext,
type ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
/**
* Authentication context interface providing access to user session state and Nhost client.
* Used throughout the React application to access authentication-related data and operations.
*/
interface AuthContextType {
/** Current authenticated user object, null if not authenticated */
user: Session["user"] | null;
/** Current session object containing tokens and user data, null if no active session */
session: Session | null;
/** Boolean indicating if user is currently authenticated */
isAuthenticated: boolean;
/** Boolean indicating if authentication state is still loading */
isLoading: boolean;
/** Nhost client instance for making authenticated requests */
nhost: NhostClient;
}
// Create React context for authentication state and nhost client
const AuthContext = createContext<AuthContextType | null>(null);
interface AuthProviderProps {
children: ReactNode;
}
/**
* AuthProvider component that provides authentication context to the React application.
*
* This component handles:
* - Initializing the Nhost client with default EventEmitterStorage
* - Managing authentication state (user, session, loading, authenticated status)
* - Cross-tab session synchronization using sessionStorage.onChange events
* - Page visibility and focus event handling to maintain session consistency
* - Client-side only session management (no server-side rendering)
*/
export const AuthProvider = ({ children }: AuthProviderProps) => {
const [user, setUser] = useState<Session["user"] | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const lastRefreshTokenIdRef = useRef<string | null>(null);
// Initialize Nhost client with default SessionStorage (local storage)
const nhost = useMemo(
() =>
createClient({
region: import.meta.env.VITE_NHOST_REGION || "local",
subdomain: import.meta.env.VITE_NHOST_SUBDOMAIN || "local",
}),
[],
);
/**
* Handles session reload when refresh token changes.
* This detects when the session has been updated from other tabs.
* Unlike the Next.js version, this only updates local state without server synchronization.
*
* @param currentRefreshTokenId - The current refresh token ID to compare against stored value
*/
const reloadSession = useCallback(
(currentRefreshTokenId: string | null) => {
if (currentRefreshTokenId !== lastRefreshTokenIdRef.current) {
lastRefreshTokenIdRef.current = currentRefreshTokenId;
// Update local authentication state to match current session
const currentSession = nhost.getUserSession();
setUser(currentSession?.user || null);
setSession(currentSession);
setIsAuthenticated(!!currentSession);
}
},
[nhost],
);
// Initialize authentication state and set up cross-tab session synchronization
useEffect(() => {
setIsLoading(true);
// Load initial session state from Nhost client
const currentSession = nhost.getUserSession();
setUser(currentSession?.user || null);
setSession(currentSession);
setIsAuthenticated(!!currentSession);
lastRefreshTokenIdRef.current = currentSession?.refreshTokenId ?? null;
setIsLoading(false);
// Subscribe to session changes from other browser tabs
// This enables real-time synchronization when user signs in/out in another tab
const unsubscribe = nhost.sessionStorage.onChange((session) => {
reloadSession(session?.refreshTokenId ?? null);
});
return unsubscribe;
}, [nhost, reloadSession]);
// Handle session changes from page focus events (for additional session consistency)
useEffect(() => {
/**
* Checks for session changes when page becomes visible or focused.
* In the React SPA context, this provides additional consistency checks
* though it's less critical than in the Next.js SSR version.
*/
const checkSessionOnFocus = () => {
reloadSession(nhost.getUserSession()?.refreshTokenId ?? null);
};
// Monitor page visibility changes (tab switching, window minimizing)
document.addEventListener("visibilitychange", () => {
if (!document.hidden) {
checkSessionOnFocus();
}
});
// Monitor window focus events (clicking back into the browser window)
window.addEventListener("focus", checkSessionOnFocus);
// Cleanup event listeners on component unmount
return () => {
document.removeEventListener("visibilitychange", checkSessionOnFocus);
window.removeEventListener("focus", checkSessionOnFocus);
};
}, [nhost, reloadSession]);
const value: AuthContextType = {
user,
session,
isAuthenticated,
isLoading,
nhost,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
/**
* Custom hook to access the authentication context.
*
* Must be used within a component wrapped by AuthProvider.
* Provides access to current user session, authentication state, and Nhost client.
*
* @throws {Error} When used outside of AuthProvider
* @returns {AuthContextType} Authentication context containing user, session, and client
*
* @example
* ```tsx
* function MyComponent() {
* const { user, isAuthenticated, nhost } = useAuth();
*
* if (!isAuthenticated) {
* return <div>Please sign in</div>;
* }
*
* return <div>Welcome, {user?.displayName}!</div>;
* }
* ```
*/
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
};

View File

@@ -0,0 +1,19 @@
import React from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";
import { AuthProvider } from "./lib/nhost/AuthProvider";
// Root component that sets up providers
const Root = () => (
<React.StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>
);
const rootElement = document.getElementById("root");
if (!rootElement) throw new Error("Root element not found");
createRoot(rootElement).render(<Root />);

View File

@@ -0,0 +1,217 @@
/* Custom styles for Ninja Turtles tabs interface */
.ninja-turtles-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.ninja-turtles-title {
text-align: center;
margin-bottom: 25px;
color: #1a9c44;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Tab navigation */
.turtle-tabs {
display: flex;
border-bottom: 2px solid #1a9c44;
margin-bottom: 20px;
overflow-x: auto;
}
.turtle-tab {
padding: 10px 20px;
margin-right: 5px;
background: none;
border: none;
cursor: pointer;
font-weight: 600;
color: var(--text-secondary);
transition: all 0.3s ease;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
position: relative;
outline: none;
}
.turtle-tab:hover {
color: var(--text-primary);
background: rgba(26, 156, 68, 0.1);
}
.turtle-tab.active {
color: white;
background: #1a9c44;
}
/* Turtle Card Styles */
.turtle-card {
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
animation: fadeIn 0.5s;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.turtle-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.turtle-name {
color: #1a9c44;
font-weight: 700;
}
.turtle-description {
margin-bottom: 20px;
line-height: 1.6;
}
.turtle-date {
font-size: 0.85rem;
margin-bottom: 20px;
color: var(--text-muted);
}
/* Comments section */
.comments-section {
margin-top: 25px;
border-top: 1px solid var(--border-color);
padding-top: 15px;
}
.comments-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 15px;
color: #1a9c44;
}
.comment-card {
margin-bottom: 15px;
padding: 12px;
border-radius: 6px;
background-color: rgba(26, 156, 68, 0.05);
border: 1px solid rgba(26, 156, 68, 0.1);
}
.comment-text {
margin-bottom: 8px;
}
.comment-meta {
display: flex;
align-items: center;
font-size: 0.8rem;
color: var(--text-muted);
}
.comment-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
background-color: #1a9c44;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
margin-right: 8px;
}
/* Comment form */
.comment-form {
margin-top: 20px;
}
.comment-textarea {
width: 100%;
background-color: rgba(31, 41, 55, 0.8);
border: 1px solid var(--border-color);
border-radius: 6px;
padding: 10px;
color: white;
transition: all 0.2s;
margin-bottom: 10px;
}
.comment-textarea:focus {
border-color: #1a9c44;
box-shadow: 0 0 0 2px rgba(26, 156, 68, 0.2);
outline: none;
}
.comment-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.cancel-button {
background-color: transparent;
border: 1px solid var(--border-color);
color: var(--text-secondary);
}
.cancel-button:hover {
background-color: rgba(31, 41, 55, 0.8);
}
.submit-button {
background-color: #1a9c44;
color: white;
}
.submit-button:hover {
background-color: #148035;
}
.add-comment-button {
display: inline-flex;
align-items: center;
color: #1a9c44;
background: none;
border: none;
padding: 5px 0;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s;
}
.add-comment-button:hover {
color: #148035;
text-decoration: underline;
}
.add-comment-button svg {
width: 14px;
height: 14px;
margin-right: 5px;
}
/* Responsive adjustments */
@media (max-width: 640px) {
.turtle-tabs {
flex-wrap: nowrap;
overflow-x: auto;
}
.turtle-tab {
flex: 0 0 auto;
}
}

View File

@@ -0,0 +1,237 @@
import { type JSX, useCallback, useEffect, useState } from "react";
import {
AddCommentDocument,
GetNinjaTurtlesWithCommentsDocument,
type GetNinjaTurtlesWithCommentsQuery,
} from "../lib/graphql/__generated__/graphql";
import { useAuth } from "../lib/nhost/AuthProvider";
import "./Home.css";
export default function Home(): JSX.Element {
const { isLoading, nhost } = useAuth();
const [activeCommentId, setActiveCommentId] = useState<string | null>(null);
const [commentText, setCommentText] = useState("");
const [activeTabId, setActiveTabId] = useState<string | null>(null);
const [data, setData] = useState<GetNinjaTurtlesWithCommentsQuery | null>(
null,
);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
// Fetch ninja turtles data
const fetchNinjaTurtles = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await nhost.graphql.request(
GetNinjaTurtlesWithCommentsDocument,
{},
);
if (result.body.errors) {
throw new Error(result.body.errors[0]?.message);
}
setData(result.body.data ?? null);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}, [nhost.graphql]);
// Load data on mount
useEffect(() => {
if (!isLoading) {
fetchNinjaTurtles();
}
}, [isLoading, fetchNinjaTurtles]);
const addComment = async (ninjaTurtleId: string, comment: string) => {
try {
const result = await nhost.graphql.request(AddCommentDocument, {
ninjaTurtleId,
comment,
});
if (result.body.errors) {
throw new Error(result.body.errors[0]?.message);
}
// Clear form and refetch data
setCommentText("");
setActiveCommentId(null);
await fetchNinjaTurtles();
} catch (err) {
console.error("Error adding comment:", err);
}
};
// If authentication is still loading, show a loading state
if (isLoading) {
return (
<div className="loading-container">
<p>Loading...</p>
</div>
);
}
const handleAddComment = (turtleId: string) => {
if (!commentText.trim()) return;
addComment(turtleId, commentText);
};
if (loading)
return (
<div className="loading-container">
<p>Loading ninja turtles...</p>
</div>
);
if (error)
return (
<div className="alert alert-error">
Error loading ninja turtles: {(error as Error).message}
</div>
);
// Access the data using the correct field name from the GraphQL response
const ninjaTurtles = data?.ninjaTurtles || [];
if (!ninjaTurtles || ninjaTurtles.length === 0) {
return (
<div className="no-turtles-container">
<p>No ninja turtles found. Please add some!</p>
</div>
);
}
// Set the active tab to the first turtle if there's no active tab and there are turtles
if (activeTabId === null) {
setActiveTabId(ninjaTurtles[0] ? ninjaTurtles[0].id : null);
}
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
};
return (
<div className="ninja-turtles-container">
<h1 className="ninja-turtles-title text-3xl font-bold mb-6">
Teenage Mutant Ninja Turtles
</h1>
{/* Tabs navigation */}
<div className="turtle-tabs">
{ninjaTurtles.map((turtle) => (
<button
key={turtle.id}
type="button"
className={`turtle-tab ${activeTabId === turtle.id ? "active" : ""}`}
onClick={() => setActiveTabId(turtle.id)}
>
{turtle.name}
</button>
))}
</div>
{/* Display active turtle */}
{ninjaTurtles
.filter((turtle) => turtle.id === activeTabId)
.map((turtle) => (
<div key={turtle.id} className="turtle-card glass-card p-6">
<div className="turtle-header">
<h2 className="turtle-name text-2xl font-semibold">
{turtle.name}
</h2>
</div>
<p className="turtle-description">{turtle.description}</p>
<div className="turtle-date">
Added on {formatDate(turtle.createdAt || turtle.createdAt)}
</div>
<div className="comments-section">
<h3 className="comments-title">
Comments ({turtle.comments.length})
</h3>
{turtle.comments.map((comment) => (
<div key={comment.id} className="comment-card">
<p className="comment-text">{comment.comment}</p>
<div className="comment-meta">
<div className="comment-avatar">
{(comment.user?.displayName || comment.user?.email || "?")
.charAt(0)
.toUpperCase()}
</div>
<p>
{comment.user?.displayName ||
comment.user?.email ||
"Anonymous"}{" "}
- {formatDate(comment.createdAt || comment.createdAt)}
</p>
</div>
</div>
))}
{activeCommentId === turtle.id ? (
<div className="comment-form">
<textarea
className="comment-textarea"
value={commentText}
onChange={(e) => setCommentText(e.target.value)}
placeholder="Add your comment..."
rows={3}
/>
<div className="comment-actions">
<button
type="button"
onClick={() => {
setActiveCommentId(null);
setCommentText("");
}}
className="btn cancel-button"
>
Cancel
</button>
<button
type="button"
onClick={() => handleAddComment(turtle.id)}
className="btn submit-button"
>
Submit
</button>
</div>
</div>
) : (
<button
type="button"
onClick={() => setActiveCommentId(turtle.id)}
className="add-comment-button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Add Comment"
role="img"
>
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add a comment
</button>
)}
</div>
</div>
))}
</div>
);
}

View File

@@ -0,0 +1,66 @@
import type { JSX } from "react";
import { useAuth } from "../lib/nhost/AuthProvider";
export default function Profile(): JSX.Element {
const { user, session } = useAuth();
// ProtectedRoute component now handles authentication check
// We can just focus on the component logic here
return (
<div className="flex flex-col">
<h1 className="text-3xl mb-6 gradient-text">Your Profile</h1>
<div className="glass-card p-8 mb-6">
<div className="space-y-5">
<div className="profile-item">
<strong>Display Name:</strong>
<span className="ml-2">{user?.displayName || "Not set"}</span>
</div>
<div className="profile-item">
<strong>Email:</strong>
<span className="ml-2">{user?.email || "Not available"}</span>
</div>
<div className="profile-item">
<strong>User ID:</strong>
<span
className="ml-2"
style={{
fontFamily: "var(--font-geist-mono)",
fontSize: "0.875rem",
}}
>
{user?.id || "Not available"}
</span>
</div>
<div className="profile-item">
<strong>Roles:</strong>
<span className="ml-2">{user?.roles?.join(", ") || "None"}</span>
</div>
<div className="profile-item">
<strong>Email Verified:</strong>
<span className="ml-2">{user?.emailVerified ? "Yes" : "No"}</span>
</div>
</div>
</div>
<div className="glass-card p-8 mb-6">
<h3 className="text-xl mb-4">Session Information</h3>
<pre>
{JSON.stringify(
{
refreshTokenId: session?.refreshTokenId,
accessTokenExpiresIn: session?.accessTokenExpiresIn,
},
null,
2,
)}
</pre>
</div>
</div>
);
}

View File

@@ -0,0 +1,120 @@
import type { ErrorResponse } from "@nhost/nhost-js/auth";
import type { FetchError } from "@nhost/nhost-js/fetch";
import { type JSX, useEffect, useId, useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { useAuth } from "../lib/nhost/AuthProvider";
export default function SignIn(): JSX.Element {
const { nhost, isAuthenticated } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const params = new URLSearchParams(location.search);
const emailId = useId();
const passwordId = useId();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(
params.get("error") || null,
);
const isVerifying = params.has("fromVerify");
// Use useEffect for navigation after authentication is confirmed
useEffect(() => {
if (isAuthenticated && !isVerifying) {
navigate("/home");
}
}, [isAuthenticated, isVerifying, navigate]);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setError(null);
try {
// Use the signIn function from auth context
const response = await nhost.auth.signInEmailPassword({
email,
password,
});
// Check if MFA is required
if (response.body?.mfa) {
navigate(`/signin/mfa?ticket=${response.body.mfa.ticket}`);
return;
}
// If we have a session, sign in was successful
if (response.body?.session) {
navigate("/home");
} else {
setError("Failed to sign in");
}
} catch (err) {
const error = err as FetchError<ErrorResponse>;
setError(`An error occurred during sign in: ${error.message}`);
} finally {
setIsLoading(false);
}
};
return (
<div className="flex flex-col items-center justify-center">
<h1 className="text-3xl mb-6 gradient-text">Nhost SDK Demo</h1>
<div className="glass-card w-full p-8 mb-6">
<h2 className="text-2xl mb-6">Sign In</h2>
<div>
<div className="tabs-container">
<button type="button" className="tab-button tab-active">
Email + Password
</button>
</div>
<div className="tab-content">
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label htmlFor={emailId}>Email</label>
<input
id={emailId}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor={passwordId}>Password</label>
<input
id={passwordId}
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
{error && <div className="alert alert-error">{error}</div>}
<button
type="submit"
className="btn btn-primary w-full"
disabled={isLoading}
>
{isLoading ? "Signing In..." : "Sign In"}
</button>
</form>
</div>
</div>
</div>
<div className="mt-4">
<p>
Don&apos;t have an account? <Link to="/signup">Sign Up</Link>
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,127 @@
import type { ErrorResponse } from "@nhost/nhost-js/auth";
import type { FetchError } from "@nhost/nhost-js/fetch";
import { type JSX, useId, useState } from "react";
import { Link, Navigate, useNavigate } from "react-router-dom";
import { useAuth } from "../lib/nhost/AuthProvider";
export default function SignUp(): JSX.Element {
const { nhost, isAuthenticated } = useAuth();
const navigate = useNavigate();
const displayNameId = useId();
const emailId = useId();
const passwordId = useId();
const [email, setEmail] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [displayName, setDisplayName] = useState<string>("");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
// If already authenticated, redirect to profile
if (isAuthenticated) {
return <Navigate to="/home" />;
}
const handleSubmit = async (
e: React.FormEvent<HTMLFormElement>,
): Promise<void> => {
e.preventDefault();
setIsLoading(true);
setError(null);
try {
const response = await nhost.auth.signUpEmailPassword({
email,
password,
options: {
displayName,
},
});
if (response.body) {
// Successfully signed up and automatically signed in
navigate("/home");
} else {
// Verification email sent
navigate("/verify");
}
} catch (err) {
const error = err as FetchError<ErrorResponse>;
setError(`An error occurred during sign up: ${error.message}`);
} finally {
setIsLoading(false);
}
};
return (
<div className="flex flex-col items-center justify-center">
<h1 className="text-3xl mb-6 gradient-text">Nhost SDK Demo</h1>
<div className="glass-card w-full p-8 mb-6">
<h2 className="text-2xl mb-6">Sign Up</h2>
<div>
<div className="tabs-container">
<button type="button" className="tab-button tab-active">
Email + Password
</button>
</div>
<div className="tab-content">
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label htmlFor={displayNameId}>Display Name</label>
<input
id={displayNameId}
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
/>
</div>
<div>
<label htmlFor={emailId}>Email</label>
<input
id={emailId}
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor={passwordId}>Password</label>
<input
id={passwordId}
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<p className="text-xs mt-1 text-gray-400">
Password must be at least 8 characters long
</p>
</div>
{error && <div className="alert alert-error">{error}</div>}
<button
type="submit"
className="btn btn-primary w-full"
disabled={isLoading}
>
{isLoading ? "Signing Up..." : "Sign Up"}
</button>
</form>
</div>
</div>
</div>
<div className="mt-4">
<p>
Already have an account? <Link to="/signin">Sign In</Link>
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,11 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_NHOST_REGION: string | undefined;
readonly VITE_NHOST_SUBDOMAIN: string | undefined;
readonly VITE_ENV: string | undefined;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../build/configs/tsconfig/frontend.json",
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View File

@@ -0,0 +1,4 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../build/configs/tsconfig/vite.json"
}

View File

@@ -0,0 +1,7 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
});

View File

@@ -17,6 +17,8 @@ let
"pnpm-lock.yaml"
"${submodule}/package.json"
"${submodule}/pnpm-lock.yaml"
"${submodule}/codegen-nhost/package.json"
"${submodule}/codegen-nhost/pnpm-lock.yaml"
"${submodule}/react-apollo/package.json"
"${submodule}/react-apollo/pnpm-lock.yaml"
"${submodule}/react-query/package.json"

View File

@@ -1,5 +1,5 @@
{
"name": "demos/react-apollo",
"name": "guides/react-apollo",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -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

@@ -1,5 +1,5 @@
{
"name": "demos/react-query",
"name": "guides/react-query",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -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

@@ -1,5 +1,5 @@
{
"name": "demos/react-urql",
"name": "guides/react-urql",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -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,11 @@
"@types/react": "~19.1.0",
"typescript": "~5.9.2"
},
"private": true
"private": true,
"pnpm": {
"overrides": {
"js-yaml@<=4.1.0": ">=4.1.1",
"glob@>=10.3.7 <=11.0.3": ">=11.1.0"
}
}
}

View File

@@ -4,6 +4,10 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
js-yaml@<=4.1.0: '>=4.1.1'
glob@>=10.3.7 <=11.0.3: '>=11.1.0'
importers:
.:
@@ -32,6 +36,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 +48,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
@@ -690,6 +700,14 @@ packages:
resolution: {integrity: sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw==}
hasBin: 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'}
@@ -753,10 +771,6 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
@@ -1221,9 +1235,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 +1628,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'}
@@ -1846,8 +1852,9 @@ packages:
resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==}
engines: {node: '>=6'}
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.2.3:
@@ -1976,8 +1983,9 @@ packages:
resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==}
engines: {node: '>=8'}
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jackspeak@4.1.1:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22}
jest-environment-node@29.7.0:
resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==}
@@ -2021,12 +2029,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:
@@ -2157,6 +2161,10 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
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==}
@@ -2232,6 +2240,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'}
@@ -2317,6 +2329,10 @@ packages:
resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==}
engines: {node: '>=4'}
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==}
@@ -2484,9 +2500,9 @@ packages:
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
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}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -2827,9 +2843,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'}
@@ -3844,7 +3857,7 @@ snapshots:
expo: 54.0.9(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.7)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0))(react@19.1.0)
freeport-async: 2.0.0
getenv: 2.0.0
glob: 10.4.5
glob: 12.0.0
lan-network: 0.1.7
minimatch: 9.0.5
node-forge: 1.3.1
@@ -3896,7 +3909,7 @@ snapshots:
chalk: 4.1.2
debug: 4.4.3
getenv: 2.0.0
glob: 10.4.5
glob: 12.0.0
resolve-from: 5.0.0
semver: 7.7.2
slash: 3.0.0
@@ -3916,7 +3929,7 @@ snapshots:
'@expo/json-file': 10.0.7
deepmerge: 4.3.1
getenv: 2.0.0
glob: 10.4.5
glob: 12.0.0
require-from-string: 2.0.2
resolve-from: 5.0.0
resolve-workspace-root: 2.0.0
@@ -3930,7 +3943,7 @@ snapshots:
dependencies:
'@expo/sudo-prompt': 9.3.2
debug: 3.2.7
glob: 10.4.5
glob: 12.0.0
transitivePeerDependencies:
- supports-color
@@ -3958,7 +3971,7 @@ snapshots:
chalk: 4.1.2
debug: 4.4.3
getenv: 2.0.0
glob: 10.4.5
glob: 12.0.0
ignore: 5.3.2
minimatch: 9.0.5
p-limit: 3.1.0
@@ -4010,7 +4023,7 @@ snapshots:
dotenv: 16.4.7
dotenv-expand: 11.0.7
getenv: 2.0.0
glob: 10.4.5
glob: 12.0.0
hermes-parser: 0.29.1
jsc-safe-url: 0.2.4
lightningcss: 1.30.1
@@ -4121,7 +4134,13 @@ 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/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0':
dependencies:
'@isaacs/balanced-match': 4.0.1
'@isaacs/cliui@8.0.2':
dependencies:
@@ -4143,7 +4162,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': {}
@@ -4225,9 +4244,6 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@pkgjs/parseargs@0.11.0':
optional: true
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-collection@1.1.7(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)':
@@ -4726,10 +4742,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 +5073,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 +5158,6 @@ snapshots:
escape-string-regexp@4.0.0: {}
esprima@4.0.1: {}
etag@1.8.1: {}
event-target-shim@5.0.1: {}
@@ -5224,7 +5234,7 @@ snapshots:
'@expo/spawn-async': 1.7.2
chalk: 4.1.2
commander: 7.2.0
glob: 10.4.5
glob: 12.0.0
require-from-string: 2.0.2
resolve-from: 5.0.0
@@ -5388,14 +5398,14 @@ snapshots:
getenv@2.0.0: {}
glob@10.4.5:
glob@12.0.0:
dependencies:
foreground-child: 3.3.1
jackspeak: 3.4.3
minimatch: 9.0.5
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.2.3:
dependencies:
@@ -5515,11 +5525,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
jackspeak@3.4.3:
jackspeak@4.1.1:
dependencies:
'@isaacs/cliui': 8.0.2
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jest-environment-node@29.7.0:
dependencies:
@@ -5597,12 +5605,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
@@ -5698,6 +5701,8 @@ snapshots:
lru-cache@10.4.3: {}
lru-cache@11.2.2: {}
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
@@ -5840,6 +5845,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
@@ -6083,6 +6093,10 @@ snapshots:
mimic-fn@1.2.0: {}
minimatch@10.1.1:
dependencies:
'@isaacs/brace-expansion': 5.0.0
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
@@ -6225,9 +6239,9 @@ snapshots:
path-parse@1.0.7: {}
path-scurry@1.11.1:
path-scurry@2.0.1:
dependencies:
lru-cache: 10.4.3
lru-cache: 11.2.2
minipass: 7.1.2
picocolors@1.1.1: {}
@@ -6588,8 +6602,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
@@ -6640,7 +6652,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.7

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:

13
go.mod
View File

@@ -13,7 +13,7 @@ require (
github.com/bradfitz/gomemcache v0.0.0-20250403215159-8d39553ac7cf
github.com/charmbracelet/lipgloss v1.1.0
github.com/creack/pty v1.1.24
github.com/davidbyttow/govips/v2 v2.16.0
github.com/cshum/vipsgen v1.2.1
github.com/gabriel-vasile/mimetype v1.4.8
github.com/getkin/kin-openapi v0.133.0
github.com/gin-gonic/gin v1.11.0
@@ -43,10 +43,10 @@ require (
github.com/vektah/gqlparser/v2 v2.5.30
github.com/wI2L/jsondiff v0.7.0
go.uber.org/mock v0.5.0
golang.org/x/crypto v0.42.0
golang.org/x/crypto v0.43.0
golang.org/x/mod v0.28.0
golang.org/x/oauth2 v0.30.0
golang.org/x/term v0.35.0
golang.org/x/term v0.36.0
gopkg.in/evanphx/json-patch.v5 v5.9.11
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
@@ -179,11 +179,10 @@ require (
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
golang.org/x/arch v0.20.0 // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/net v0.45.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect

66
go.sum
View File

@@ -121,14 +121,14 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cshum/vipsgen v1.2.1 h1:Es305Zf7C9T+8QbsiWn3BtQ+2/uHz6sp/SFnvwnO/kU=
github.com/cshum/vipsgen v1.2.1/go.mod h1:1GboZQcNmo4NwuNnGogM24m3O+1i6UpnvurqMcsFItE=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davidbyttow/govips/v2 v2.16.0 h1:1nH/Rbx8qZP1hd+oYL9fYQjAnm1+KorX9s07ZGseQmo=
github.com/davidbyttow/govips/v2 v2.16.0/go.mod h1:clH5/IDVmG5eVyc23qYpyi7kmOT0B/1QNTKtci4RkyM=
github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
@@ -223,7 +223,6 @@ github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+Licev
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.3 h1:+yx0/anQuGzi+ssRqeD6WpXjW2L/V0dItUayO0i9sRc=
@@ -415,7 +414,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -465,7 +463,6 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
@@ -483,23 +480,14 @@ golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -508,17 +496,12 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
@@ -526,11 +509,6 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -544,50 +522,31 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -600,7 +559,6 @@ google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXn
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v5 v5.9.11 h1:OMPeiLomOQwe8+Ku4nwXsdOmrRw2vGUpP3XgLj3ojNw=

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

@@ -275,7 +275,8 @@ export type OutputImageFormat =
| "jpeg"
| "webp"
| "png"
| "avif";
| "avif"
| "heic";
/**
*
@@ -378,7 +379,7 @@ export interface ListOrphanedFilesResponse200 {
/**
* Parameters for the getFile method.
@property q? (number) - Image quality (1-100). Only applies to JPEG, WebP and PNG files
@property q? (number) - Image quality (1-100). Only applies to JPEG, WebP, PNG and HEIC files
@property h? (number) - Maximum height to resize image to while maintaining aspect ratio. Only applies to image files
@@ -391,7 +392,7 @@ export interface ListOrphanedFilesResponse200 {
* Output format for image files. Use 'auto' for content negotiation based on Accept header*/
export interface GetFileParams {
/**
* Image quality (1-100). Only applies to JPEG, WebP and PNG files
* Image quality (1-100). Only applies to JPEG, WebP, PNG and HEIC files
*/
q?: number;
@@ -419,7 +420,7 @@ export interface GetFileParams {
}
/**
* Parameters for the getFileMetadataHeaders method.
@property q? (number) - Image quality (1-100). Only applies to JPEG, WebP and PNG files
@property q? (number) - Image quality (1-100). Only applies to JPEG, WebP, PNG and HEIC files
@property h? (number) - Maximum height to resize image to while maintaining aspect ratio. Only applies to image files
@@ -432,7 +433,7 @@ export interface GetFileParams {
* Output format for image files. Use 'auto' for content negotiation based on Accept header*/
export interface GetFileMetadataHeadersParams {
/**
* Image quality (1-100). Only applies to JPEG, WebP and PNG files
* Image quality (1-100). Only applies to JPEG, WebP, PNG and HEIC files
*/
q?: number;
@@ -632,16 +633,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 +811,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

@@ -1,3 +1,9 @@
## [storage@0.10.0] - 2025-11-19
### 🚀 Features
- *(storage)* Added support for images/heic (#3694)
## [storage@0.9.1] - 2025-11-06
### 🐛 Bug Fixes

View File

@@ -2066,83 +2066,84 @@ func (sh *strictHandler) GetVersion(ctx *gin.Context) {
// Base64 encoded, gzipped, json marshaled Swagger object
var swaggerSpec = []string{
"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",
"AA==",
}
// GetSwagger returns the content of the embedded swagger specification file

View File

@@ -18,6 +18,7 @@ const (
const (
Auto OutputImageFormat = "auto"
Avif OutputImageFormat = "avif"
Heic OutputImageFormat = "heic"
Jpeg OutputImageFormat = "jpeg"
Png OutputImageFormat = "png"
Same OutputImageFormat = "same"
@@ -158,7 +159,7 @@ type UploadFilesMultipartBody struct {
// GetFileParams defines parameters for GetFile.
type GetFileParams struct {
// Q Image quality (1-100). Only applies to JPEG, WebP and PNG files
// Q Image quality (1-100). Only applies to JPEG, WebP, PNG and HEIC files
Q *int `form:"q,omitempty" json:"q,omitempty"`
// H Maximum height to resize image to while maintaining aspect ratio. Only applies to image files
@@ -191,7 +192,7 @@ type GetFileParams struct {
// GetFileMetadataHeadersParams defines parameters for GetFileMetadataHeaders.
type GetFileMetadataHeadersParams struct {
// Q Image quality (1-100). Only applies to JPEG, WebP and PNG files
// Q Image quality (1-100). Only applies to JPEG, WebP, PNG and HEIC files
Q *int `form:"q,omitempty" json:"q,omitempty"`
// H Maximum height to resize image to while maintaining aspect ratio. Only applies to image files
@@ -257,7 +258,7 @@ type GetFileWithPresignedURLParams struct {
// XId Use presignedurl endpoint to generate this automatically
XId string `form:"x-id" json:"x-id"`
// Q Image quality (1-100). Only applies to JPEG, WebP and PNG files
// Q Image quality (1-100). Only applies to JPEG, WebP, PNG and HEIC files
Q *int `form:"q,omitempty" json:"q,omitempty"`
// H Maximum height to resize image to while maintaining aspect ratio. Only applies to image files

Some files were not shown because too many files have changed in this diff Show More