Compare commits
10 Commits
storage@0.
...
release/pa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c9afc8980 | ||
|
|
ab890d8593 | ||
|
|
ee2d9763f7 | ||
|
|
f7ea20db61 | ||
|
|
99ac1aee3a | ||
|
|
bb9aaf2903 | ||
|
|
8e82edd0c6 | ||
|
|
b0256da33f | ||
|
|
351daa5fbe | ||
|
|
4e9de6a764 |
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -233,7 +233,8 @@
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild@<=0.24.2": ">=0.25.0",
|
||||
"js-yaml@<=4.1.0": ">=4.1.1"
|
||||
"js-yaml@<=4.1.0": ">=4.1.1",
|
||||
"glob@>=10.3.7 <=11.0.3": ">=11.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
99
dashboard/pnpm-lock.yaml
generated
99
dashboard/pnpm-lock.yaml
generated
@@ -7,6 +7,7 @@ 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=
|
||||
|
||||
@@ -2246,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'}
|
||||
@@ -2619,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'}
|
||||
@@ -5650,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:
|
||||
@@ -5780,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:
|
||||
@@ -6289,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==}
|
||||
@@ -6556,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==}
|
||||
@@ -6799,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==}
|
||||
|
||||
@@ -6810,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'}
|
||||
@@ -7182,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==}
|
||||
@@ -11153,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
|
||||
@@ -11605,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
|
||||
@@ -15242,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
|
||||
@@ -15383,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:
|
||||
@@ -15913,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:
|
||||
@@ -16211,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:
|
||||
@@ -16641,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
|
||||
@@ -16653,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
|
||||
@@ -17095,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: {}
|
||||
@@ -17943,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:
|
||||
@@ -18062,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
|
||||
@@ -18164,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: {}
|
||||
@@ -18790,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:
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as getFormattedServiceConfig } from './getFormattedServiceConfig';
|
||||
@@ -0,0 +1 @@
|
||||
export { default as parseConfigFromInstallLink } from './parseConfigFromInstallLink';
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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: (
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"js-yaml@<=4.1.0": ">=4.1.1"
|
||||
"js-yaml@<=4.1.0": ">=4.1.1",
|
||||
"glob@>=10.3.7 <=11.0.3": ">=11.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
71
docs/pnpm-lock.yaml
generated
71
docs/pnpm-lock.yaml
generated
@@ -6,6 +6,7 @@ settings:
|
||||
|
||||
overrides:
|
||||
js-yaml@<=4.1.0: '>=4.1.1'
|
||||
glob@>=10.3.7 <=11.0.3: '>=11.1.0'
|
||||
|
||||
packageExtensionsChecksum: sha256-4+NJJHoeDEOtWI2UxgTNLimXyrOojBs00S85/9Babm0=
|
||||
|
||||
@@ -319,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'}
|
||||
@@ -423,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'}
|
||||
@@ -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==}
|
||||
@@ -2183,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==}
|
||||
@@ -2424,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==}
|
||||
|
||||
@@ -2637,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==}
|
||||
@@ -3842,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
|
||||
@@ -4222,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
|
||||
@@ -5644,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:
|
||||
@@ -6164,11 +6179,9 @@ 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: {}
|
||||
|
||||
@@ -6236,7 +6249,7 @@ snapshots:
|
||||
|
||||
lowercase-keys@3.0.0: {}
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
lru-cache@11.2.2: {}
|
||||
|
||||
lru-cache@7.18.3: {}
|
||||
|
||||
@@ -6755,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
|
||||
@@ -6989,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: {}
|
||||
@@ -7744,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
244
docs/products/auth/providers/sign-in-provider.mdx
Normal file
244
docs/products/auth/providers/sign-in-provider.mdx
Normal 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
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
9
docs/products/graphql/guides/codegen-nhost.mdx
Normal file
9
docs/products/graphql/guides/codegen-nhost.mdx
Normal 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).
|
||||
@@ -57,7 +57,8 @@
|
||||
"private": true,
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"js-yaml@<=4.1.0": ">=4.1.1"
|
||||
"js-yaml@<=4.1.0": ">=4.1.1",
|
||||
"glob@>=10.3.7 <=11.0.3": ">=11.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
84
examples/demos/ReactNativeDemo/pnpm-lock.yaml
generated
84
examples/demos/ReactNativeDemo/pnpm-lock.yaml
generated
@@ -6,6 +6,7 @@ settings:
|
||||
|
||||
overrides:
|
||||
js-yaml@<=4.1.0: '>=4.1.1'
|
||||
glob@>=10.3.7 <=11.0.3: '>=11.1.0'
|
||||
|
||||
importers:
|
||||
|
||||
@@ -716,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'}
|
||||
@@ -776,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:
|
||||
@@ -1698,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:
|
||||
@@ -1827,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==}
|
||||
@@ -2007,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==}
|
||||
|
||||
@@ -2109,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==}
|
||||
|
||||
@@ -2272,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==}
|
||||
@@ -3588,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
|
||||
@@ -3636,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
|
||||
@@ -3656,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
|
||||
@@ -3670,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
|
||||
|
||||
@@ -3692,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
|
||||
@@ -3733,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
|
||||
@@ -3815,6 +3830,12 @@ snapshots:
|
||||
find-up: 5.0.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:
|
||||
string-width: 5.1.2
|
||||
@@ -3912,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
|
||||
@@ -4775,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
|
||||
|
||||
@@ -4951,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:
|
||||
@@ -5078,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:
|
||||
@@ -5258,6 +5274,8 @@ snapshots:
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.2: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
@@ -5468,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
|
||||
@@ -5606,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: {}
|
||||
@@ -6038,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
|
||||
|
||||
25
examples/guides/codegen-nhost/.gitignore
vendored
Normal file
25
examples/guides/codegen-nhost/.gitignore
vendored
Normal 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
|
||||
443
examples/guides/codegen-nhost/README.md
Normal file
443
examples/guides/codegen-nhost/README.md
Normal 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)
|
||||
44
examples/guides/codegen-nhost/add-query-source-plugin.cjs
Normal file
44
examples/guides/codegen-nhost/add-query-source-plugin.cjs
Normal 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 };
|
||||
7
examples/guides/codegen-nhost/biome.json
Normal file
7
examples/guides/codegen-nhost/biome.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"root": false,
|
||||
"extends": "//",
|
||||
"linter": {
|
||||
"includes": ["**", "!src/lib/graphql/__generated__/*.ts"]
|
||||
}
|
||||
}
|
||||
27
examples/guides/codegen-nhost/codegen-wrapper.sh
Executable file
27
examples/guides/codegen-nhost/codegen-wrapper.sh
Executable 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."
|
||||
48
examples/guides/codegen-nhost/codegen.ts
Normal file
48
examples/guides/codegen-nhost/codegen.ts
Normal 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;
|
||||
13
examples/guides/codegen-nhost/index.html
Normal file
13
examples/guides/codegen-nhost/index.html
Normal 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>
|
||||
40
examples/guides/codegen-nhost/package.json
Normal file
40
examples/guides/codegen-nhost/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
3826
examples/guides/codegen-nhost/pnpm-lock.yaml
generated
Normal file
3826
examples/guides/codegen-nhost/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
examples/guides/codegen-nhost/public/vite.svg
Normal file
1
examples/guides/codegen-nhost/public/vite.svg
Normal 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 |
10143
examples/guides/codegen-nhost/schema.graphql
Normal file
10143
examples/guides/codegen-nhost/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
56
examples/guides/codegen-nhost/src/App.tsx
Normal file
56
examples/guides/codegen-nhost/src/App.tsx
Normal 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;
|
||||
85
examples/guides/codegen-nhost/src/components/Navigation.tsx
Normal file
85
examples/guides/codegen-nhost/src/components/Navigation.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
552
examples/guides/codegen-nhost/src/index.css
Normal file
552
examples/guides/codegen-nhost/src/index.css
Normal 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;
|
||||
}
|
||||
110
examples/guides/codegen-nhost/src/lib/graphql/__generated__/fragment-masking.ts
generated
Normal file
110
examples/guides/codegen-nhost/src/lib/graphql/__generated__/fragment-masking.ts
generated
Normal 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);
|
||||
}
|
||||
51
examples/guides/codegen-nhost/src/lib/graphql/__generated__/gql.ts
generated
Normal file
51
examples/guides/codegen-nhost/src/lib/graphql/__generated__/gql.ts
generated
Normal 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;
|
||||
7026
examples/guides/codegen-nhost/src/lib/graphql/__generated__/graphql.ts
generated
Normal file
7026
examples/guides/codegen-nhost/src/lib/graphql/__generated__/graphql.ts
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
examples/guides/codegen-nhost/src/lib/graphql/__generated__/index.ts
generated
Normal file
2
examples/guides/codegen-nhost/src/lib/graphql/__generated__/index.ts
generated
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./fragment-masking";
|
||||
export * from "./gql";
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
175
examples/guides/codegen-nhost/src/lib/nhost/AuthProvider.tsx
Normal file
175
examples/guides/codegen-nhost/src/lib/nhost/AuthProvider.tsx
Normal 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;
|
||||
};
|
||||
19
examples/guides/codegen-nhost/src/main.tsx
Normal file
19
examples/guides/codegen-nhost/src/main.tsx
Normal 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 />);
|
||||
217
examples/guides/codegen-nhost/src/pages/Home.css
Normal file
217
examples/guides/codegen-nhost/src/pages/Home.css
Normal 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;
|
||||
}
|
||||
}
|
||||
237
examples/guides/codegen-nhost/src/pages/Home.tsx
Normal file
237
examples/guides/codegen-nhost/src/pages/Home.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
66
examples/guides/codegen-nhost/src/pages/Profile.tsx
Normal file
66
examples/guides/codegen-nhost/src/pages/Profile.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
120
examples/guides/codegen-nhost/src/pages/SignIn.tsx
Normal file
120
examples/guides/codegen-nhost/src/pages/SignIn.tsx
Normal 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't have an account? <Link to="/signup">Sign Up</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
127
examples/guides/codegen-nhost/src/pages/SignUp.tsx
Normal file
127
examples/guides/codegen-nhost/src/pages/SignUp.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
11
examples/guides/codegen-nhost/src/vite-env.d.ts
vendored
Normal file
11
examples/guides/codegen-nhost/src/vite-env.d.ts
vendored
Normal 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;
|
||||
}
|
||||
6
examples/guides/codegen-nhost/tsconfig.json
Normal file
6
examples/guides/codegen-nhost/tsconfig.json
Normal 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" }]
|
||||
}
|
||||
4
examples/guides/codegen-nhost/tsconfig.node.json
Normal file
4
examples/guides/codegen-nhost/tsconfig.node.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../build/configs/tsconfig/vite.json"
|
||||
}
|
||||
7
examples/guides/codegen-nhost/vite.config.ts
Normal file
7
examples/guides/codegen-nhost/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "demos/react-apollo",
|
||||
"name": "guides/react-apollo",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "demos/react-query",
|
||||
"name": "guides/react-query",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "demos/react-urql",
|
||||
"name": "guides/react-urql",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"private": true,
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"js-yaml@<=4.1.0": ">=4.1.1"
|
||||
"js-yaml@<=4.1.0": ">=4.1.1",
|
||||
"glob@>=10.3.7 <=11.0.3": ">=11.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ settings:
|
||||
|
||||
overrides:
|
||||
js-yaml@<=4.1.0: '>=4.1.1'
|
||||
glob@>=10.3.7 <=11.0.3: '>=11.1.0'
|
||||
|
||||
importers:
|
||||
|
||||
@@ -699,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'}
|
||||
@@ -762,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==}
|
||||
|
||||
@@ -1847,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:
|
||||
@@ -1977,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==}
|
||||
@@ -2154,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==}
|
||||
|
||||
@@ -2318,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==}
|
||||
|
||||
@@ -2485,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==}
|
||||
@@ -3842,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
|
||||
@@ -3894,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
|
||||
@@ -3914,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
|
||||
@@ -3928,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
|
||||
|
||||
@@ -3956,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
|
||||
@@ -4008,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,6 +4136,12 @@ snapshots:
|
||||
find-up: 5.0.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:
|
||||
string-width: 5.1.2
|
||||
@@ -4223,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)':
|
||||
@@ -5216,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
|
||||
|
||||
@@ -5380,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:
|
||||
@@ -5507,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:
|
||||
@@ -5685,6 +5701,8 @@ snapshots:
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.2: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
@@ -6075,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
|
||||
@@ -6217,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: {}
|
||||
@@ -6630,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
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
## [@nhost/nhost-js@4.2.0] - 2025-11-19
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
- *(storage)* Added support for images/heic (#3694)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- *(auth)* Return meaningful error if the provider's account is already linked (#3680)
|
||||
- *(packages/nhost-js)* React native needs special treatment when using FormData (#3697)
|
||||
|
||||
## [@nhost/nhost-js@4.1.0] - 2025-11-04
|
||||
|
||||
### 🚀 Features
|
||||
|
||||
Reference in New Issue
Block a user