Compare commits

...

11 Commits

Author SHA1 Message Date
David BM
1060cc2313 Merge branch 'main' of https://github.com/nhost/nhost into feat/modify-table-fields 2025-11-19 10:22:22 +01:00
David BM
e79d81cb68 wip 2025-11-19 10:21:55 +01:00
github-actions[bot]
0c63480cc7 release(cli): 1.34.8 (#3713)
Co-authored-by: dbarrosop <dbarrosop@users.noreply.github.com>
2025-11-19 08:48:07 +01:00
David Barroso
ab890d8593 fix(cli): update traefik (#3710) 2025-11-19 08:45:12 +01:00
David Barroso
ee2d9763f7 feat(storage): added support for images/heic (#3694) 2025-11-19 08:21:43 +01:00
David Barroso
f7ea20db61 feat(docs): added guide to use the guild's codegen directly with Nhost (#3696) 2025-11-19 08:21:30 +01:00
David BM
99ac1aee3a chore(deps): update glob in packages to address vulnerability audit (#3711) 2025-11-18 15:43:12 +01:00
David BM
bb9aaf2903 fix(dashboard): parse and create one-click installs for run services correctly (#3679) 2025-11-18 10:10:26 +01:00
David BM
8e82edd0c6 chore(deps): update blob to address security advisory (#3704) 2025-11-18 09:10:40 +01:00
David BM
b0256da33f chore(docs): fix typos and broken link (#3703) 2025-11-17 12:10:36 +01:00
Moritz Klack
351daa5fbe chore(docs): typo (#3702) 2025-11-17 12:07:29 +01:00
219 changed files with 51863 additions and 160933 deletions

View File

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

View File

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

View File

@@ -1,3 +1,9 @@
## [cli@1.34.8] - 2025-11-19
### 🐛 Bug Fixes
- *(cli)* Update traefik (#3710)
## [cli@1.34.7] - 2025-11-13
### ⚙️ Miscellaneous Tasks

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,263 @@
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
import { Alert } from '@/components/ui/v2/Alert';
import { Button } from '@/components/ui/v3/button';
import { Form } from '@/components/ui/v3/form';
import type {
BaseTableFormProps,
BaseTableFormValues,
} from '@/features/orgs/projects/database/dataGrid/components/BaseTableForm';
import {
BaseTableForm,
baseTableValidationSchema,
} from '@/features/orgs/projects/database/dataGrid/components/BaseTableForm';
import { useTableQuery } from '@/features/orgs/projects/database/dataGrid/hooks/useTableQuery';
import { useTrackForeignKeyRelationsMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useTrackForeignKeyRelationsMutation';
import { useUpdateTableMutation } from '@/features/orgs/projects/database/dataGrid/hooks/useUpdateTableMutation';
import type {
DatabaseTable,
NormalizedQueryDataRow,
} from '@/features/orgs/projects/database/dataGrid/types/dataBrowser';
import { normalizeDatabaseColumn } from '@/features/orgs/projects/database/dataGrid/utils/normalizeDatabaseColumn';
import { isNotEmptyValue } from '@/lib/utils';
import { triggerToast } from '@/utils/toast';
import { yupResolver } from '@hookform/resolvers/yup';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import type * as Yup from 'yup';
import { defaultFormValues, EditSettingsFormInitialValues, EditSettingsFormValues, validationSchema } from './EditSettingsFormTypes';
import { zodResolver } from '@hookform/resolvers/zod';
export interface EditSettingsFormProps
extends Pick<BaseTableFormProps, 'onCancel' | 'location'> {
/**
* Schema where the table is located.
*/
schema: string;
/**
* Table to be edited.
*/
table: NormalizedQueryDataRow;
/**
* Function to be called when the form is submitted.
*/
onSubmit?: (tableName: string) => Promise<void>;
}
export default function EditSettingsForm({
onSubmit,
schema,
table: originalTable,
...props
}: EditSettingsFormProps) {
const [formInitialized, setFormInitialized] = useState(false);
const router = useRouter();
const {
data,
status: columnsStatus,
error: columnsError,
} = useTableQuery([`default.${schema}.${originalTable.table_name}`], {
schema,
table: originalTable.table_name,
});
const columns = data?.columns;
const foreignKeyRelations = data?.foreignKeyRelations;
const dataGridColumns = (columns || []).map((column) =>
normalizeDatabaseColumn(column),
);
const {
mutateAsync: trackForeignKeyRelations,
error: foreignKeyError,
reset: resetForeignKeyError,
} = useTrackForeignKeyRelationsMutation();
const {
mutateAsync: updateTable,
error: updateError,
reset: resetUpdateError,
} = useUpdateTableMutation({ schema });
const error = columnsError || updateError || foreignKeyError;
function resetError() {
resetForeignKeyError();
resetUpdateError();
}
const form = useForm<
EditSettingsFormValues
>({
defaultValues: defaultFormValues,
reValidateMode: 'onSubmit',
resolver: zodResolver(validationSchema),
});
const handleFormSubmit = form.handleSubmit(async (values) => {
await onSubmit?.(values)
});
// We are initializing the form values lazily, because columns are not
// necessarily available immediately when the form is mounted.
useEffect(() => {
// if (
// columnsStatus === 'success' &&
// dataGridColumns.length > 0 &&
// !formInitialized
// ) {
// const primaryKeyIndices = dataGridColumns.reduce<string[]>(
// (result, col, index) => {
// if (col.isPrimary) {
// return [...result, `${index}`];
// }
// return result;
// },
// [],
// );
// const identityColumnIndex = dataGridColumns.findIndex(
// (column) => column.isIdentity,
// );
// form.reset({
// name: originalTable.table_name,
// columns: dataGridColumns.map((column) => ({
// // ID can't be changed through the form, so we can use it to
// // identify the column in the original array.
// id: column.id,
// name: column.id,
// type: column.type,
// defaultValue: column.defaultValue,
// isNullable: column.isNullable,
// isUnique: column.isUnique,
// comment: column.comment || '',
// })),
// primaryKeyIndices,
// identityColumnIndex:
// identityColumnIndex > -1 ? identityColumnIndex : null,
// foreignKeyRelations,
// });
// setFormInitialized(true);
}
}, [
form,
originalTable,
columnsStatus,
foreignKeyRelations,
dataGridColumns,
formInitialized,
]);
async function handleSubmit(values: BaseTableFormValues) {
const primaryKey = values.primaryKeyIndices.map<string>(
(primaryKeys) => values.columns[primaryKeys].name,
);
try {
const updatedTable: DatabaseTable = {
...values,
primaryKey,
identityColumn:
values.identityColumnIndex !== null &&
typeof values.identityColumnIndex !== 'undefined'
? values.columns[values.identityColumnIndex]?.name
: undefined,
};
await updateTable({
originalTable,
originalColumns: dataGridColumns,
originalForeignKeyRelations: foreignKeyRelations ?? [],
updatedTable,
});
if (isNotEmptyValue(updatedTable.foreignKeyRelations)) {
await trackForeignKeyRelations({
foreignKeyRelations: updatedTable.foreignKeyRelations,
schema,
table: updatedTable.name,
});
}
if (onSubmit) {
await onSubmit(updatedTable.name);
}
if (originalTable.table_name !== updatedTable.name) {
await router.push(
`/orgs/${router.query.orgSlug}/projects/${router.query.appSubdomain}/database/browser/${router.query.dataSourceSlug}/${schema}/${updatedTable.name}`,
);
}
triggerToast('The table has been updated successfully.');
} catch {
// Errors are already handled by hooks.
}
}
if (columnsStatus === 'loading') {
return (
<div className="px-6">
<ActivityIndicator label="Loading columns..." delay={1000} />
</div>
);
}
if (!formInitialized) {
return (
<div className="px-6">
<ActivityIndicator label="Loading..." delay={1000} />
</div>
);
}
if (columnsStatus === 'error') {
return (
<div className="-mt-3 px-6">
<Alert severity="error" className="text-left">
<strong>Error:</strong>{' '}
{columnsError && columnsError instanceof Error
? columnsError?.message
: 'An error occurred while loading the columns. Please try again.'}
</Alert>
</div>
);
}
return (
<Form {...form}>
<form onSubmit={handleFormSubmit}>
{error && error instanceof Error ? (
<div className="-mt-3 mb-4 px-6">
<Alert
severity="error"
className="grid grid-flow-col items-center justify-between px-4 py-3"
>
<span className="text-left">
<strong>Error:</strong> {error.message}
</span>
<Button
variant="ghost"
size="sm"
onClick={resetError}
>
Clear
</Button>
</Alert>
</div>
) : null}
<BaseTableForm
submitButtonText="Save"
onSubmit={handleSubmit}
{...props}
/>
</form>
</Form>
);
}

View File

@@ -0,0 +1,13 @@
import { z } from 'zod';
export const validationSchema = z.object({
name: z.string().min(1, { message: 'Name is required' }),
});
export const defaultFormValues: EditSettingsFormValues = {
name: '',
};
export type EditSettingsFormValues = z.infer<typeof validationSchema>;
export type EditSettingsFormInitialValues = EditSettingsFormValues;

View File

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

View File

@@ -0,0 +1,44 @@
import type { MetadataOperationOptions } from '@/features/orgs/projects/remote-schemas/types';
import { metadataOperation } from '@/utils/hasura-api/generated/default/default';
import type { SetTableIsEnumArgs } from '@/utils/hasura-api/generated/schemas';
export interface SetTableIsEnumVariables {
resourceVersion: number;
args: SetTableIsEnumArgs;
}
export default async function setTableIsEnum({
appUrl,
adminSecret,
resourceVersion,
args,
}: MetadataOperationOptions & SetTableIsEnumVariables) {
try {
const response = await metadataOperation(
{
type: 'bulk',
source: 'default',
resource_version: resourceVersion,
args: [
{
type: 'pg_set_table_is_enum',
args,
},
],
},
{
baseUrl: appUrl,
adminSecret,
},
);
if (response.status === 200) {
return response.data;
}
throw new Error(response.data.error);
} catch (error) {
console.error(error);
throw error;
}
}

View File

@@ -0,0 +1,16 @@
import type { MetadataOperationOptions } from '@/features/orgs/projects/remote-schemas/types';
import type { SetTableIsEnumArgs } from '@/utils/hasura-api/generated/schemas';
export interface SetTableIsEnumMigrationVariables {
resourceVersion: number;
args: SetTableIsEnumArgs;
}
export default async function setTableIsEnumMigration({
appUrl,
adminSecret,
resourceVersion,
args,
}: MetadataOperationOptions & SetTableIsEnumMigrationVariables) {
// TODO: Implement set table is enum migration
}

View File

@@ -0,0 +1,66 @@
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/generateAppServiceUrl';
import { useProject } from '@/features/orgs/projects/hooks/useProject';
import type { MetadataOperation200 } from '@/utils/hasura-api/generated/schemas/metadataOperation200';
import type { SuccessResponse } from '@/utils/hasura-api/generated/schemas/successResponse';
import type { MutationOptions } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import type { SetTableIsEnumVariables } from './setTableIsEnum';
import setTableIsEnum from './setTableIsEnum';
import type { SetTableIsEnumMigrationVariables } from './setTableIsEnumMigration';
import setTableIsEnumMigration from './setTableIsEnumMigration';
export interface UseSetTableIsEnumMutationOptions {
/**
* Props passed to the underlying mutation hook.
*/
mutationOptions?: MutationOptions<
SuccessResponse | MetadataOperation200,
unknown,
SetTableIsEnumVariables | SetTableIsEnumMigrationVariables
>;
}
/**
* This hook is a wrapper around a fetch call that adds a remote schema permission.
*
* @param mutationOptions - Options to use for the mutation.
* @returns The result of the mutation.
*/
export default function useSetTableIsEnumMutation({
mutationOptions,
}: UseSetTableIsEnumMutationOptions = {}) {
const { project } = useProject();
const isPlatform = useIsPlatform();
const mutation = useMutation<
SuccessResponse | MetadataOperation200,
unknown,
SetTableIsEnumVariables | SetTableIsEnumMigrationVariables
>((variables) => {
const appUrl = generateAppServiceUrl(
project!.subdomain,
project!.region,
'hasura',
);
const base = {
appUrl,
adminSecret: project?.config?.hasura.adminSecret!,
} as const;
if (isPlatform) {
return setTableIsEnum({
...(variables as SetTableIsEnumVariables),
...base,
});
}
return setTableIsEnumMigration({
...(variables as SetTableIsEnumMigrationVariables),
...base,
});
}, mutationOptions);
return mutation;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,12 @@
/**
* Generated by orval v7.11.2 🍺
* Do not edit manually.
* Hasura Remote Schema API
* API for managing remote schemas in Hasura
* OpenAPI spec version: 1.0.0
*/
import type { ExportMetadataResponseMetadataSourcesItemTablesItemTable } from './exportMetadataResponseMetadataSourcesItemTablesItemTable';
export type ExportMetadataResponseMetadataSourcesItemTablesItem = {
table: ExportMetadataResponseMetadataSourcesItemTablesItemTable;
};

View File

@@ -0,0 +1,15 @@
/**
* Generated by orval v7.11.2 🍺
* Do not edit manually.
* Hasura Remote Schema API
* API for managing remote schemas in Hasura
* OpenAPI spec version: 1.0.0
*/
export type ExportMetadataResponseMetadataSourcesItemTablesItemTable = {
/** Name of the table */
name?: string;
/** Schema of the table */
schema?: string;
[key: string]: unknown;
};

View File

@@ -0,0 +1,17 @@
/**
* Generated by orval v7.11.2 🍺
* Do not edit manually.
* Hasura Remote Schema API
* API for managing remote schemas in Hasura
* OpenAPI spec version: 1.0.0
*/
/**
* Object with table name and schema
*/
export interface QualifiedTable {
/** Name of the table */
name: string;
/** Schema of the table */
schema: string;
}

View File

@@ -0,0 +1,16 @@
/**
* Generated by orval v7.11.2 🍺
* Do not edit manually.
* Hasura Remote Schema API
* API for managing remote schemas in Hasura
* OpenAPI spec version: 1.0.0
*/
import type { QualifiedTable } from './qualifiedTable';
export interface SetTableIsEnumArgs {
table: QualifiedTable;
/** Whether or not the table should be used as an enum table <enum table>. */
is_enum: boolean;
/** Name of the source database of the table */
source?: string;
}

View File

@@ -0,0 +1,19 @@
/**
* Generated by orval v7.11.2 🍺
* Do not edit manually.
* Hasura Remote Schema API
* API for managing remote schemas in Hasura
* OpenAPI spec version: 1.0.0
*/
import type { SetTableIsEnumBulkOperationType } from './setTableIsEnumBulkOperationType';
import type { SetTableIsEnumStep } from './setTableIsEnumStep';
export interface SetTableIsEnumBulkOperation {
/** Type of operation to set table is enum */
type: SetTableIsEnumBulkOperationType;
/** Source of the operation */
source: string;
/** Resource version of the metadata */
resource_version: number;
args: SetTableIsEnumStep[];
}

View File

@@ -0,0 +1,18 @@
/**
* Generated by orval v7.11.2 🍺
* Do not edit manually.
* Hasura Remote Schema API
* API for managing remote schemas in Hasura
* OpenAPI spec version: 1.0.0
*/
/**
* Type of operation to set table is enum
*/
export type SetTableIsEnumBulkOperationType =
(typeof SetTableIsEnumBulkOperationType)[keyof typeof SetTableIsEnumBulkOperationType];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const SetTableIsEnumBulkOperationType = {
bulk: 'bulk',
} as const;

View File

@@ -0,0 +1,14 @@
/**
* Generated by orval v7.11.2 🍺
* Do not edit manually.
* Hasura Remote Schema API
* API for managing remote schemas in Hasura
* OpenAPI spec version: 1.0.0
*/
import type { SetTableIsEnumArgs } from './setTableIsEnumArgs';
import type { SetTableIsEnumStepType } from './setTableIsEnumStepType';
export interface SetTableIsEnumStep {
type: SetTableIsEnumStepType;
args: SetTableIsEnumArgs;
}

View File

@@ -0,0 +1,15 @@
/**
* Generated by orval v7.11.2 🍺
* Do not edit manually.
* Hasura Remote Schema API
* API for managing remote schemas in Hasura
* OpenAPI spec version: 1.0.0
*/
export type SetTableIsEnumStepType =
(typeof SetTableIsEnumStepType)[keyof typeof SetTableIsEnumStepType];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const SetTableIsEnumStepType = {
pg_set_table_is_enum: 'pg_set_table_is_enum',
} as const;

View File

@@ -220,7 +220,8 @@
"pages": [
"/products/graphql/guides/react-apollo",
"/products/graphql/guides/react-query",
"/products/graphql/guides/react-urql"
"/products/graphql/guides/react-urql",
"/products/graphql/guides/codegen-nhost"
]
}
]

View File

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

View File

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

View File

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

71
docs/pnpm-lock.yaml generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
{
"name": "demos/react-apollo",
"name": "guides/react-apollo",
"private": true,
"version": "0.0.0",
"type": "module",

View File

@@ -1,5 +1,5 @@
{
"name": "demos/react-query",
"name": "guides/react-query",
"private": true,
"version": "0.0.0",
"type": "module",

View File

@@ -1,5 +1,5 @@
{
"name": "demos/react-urql",
"name": "guides/react-urql",
"private": true,
"version": "0.0.0",
"type": "module",

View File

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

View File

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

13
go.mod
View File

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

66
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -283,7 +283,7 @@ func TestGetFileMetadataHeaders(t *testing.T) { //nolint:maintidx
"Accept-Ranges": []string{"bytes"},
"Cache-Control": []string{"max-age=3600"},
"Content-Disposition": []string{`inline; filename="nhost.jpg"`},
"Content-Length": []string{"8709"},
"Content-Length": []string{"8963"},
"Content-Type": []string{"image/jpeg"},
"Date": []string{"Mon, 21 Jul 2025 13:24:53 GMT"},
"Etag": []string{`"78b676e65ebc31f0bb1f2f0d05098572"`},

View File

@@ -399,7 +399,7 @@ func TestGetFile(t *testing.T) { //nolint:maintidx
"Accept-Ranges": []string{"bytes"},
"Cache-Control": []string{"max-age=3600"},
"Content-Disposition": []string{`inline; filename="nhost.jpg"`},
"Content-Length": []string{"8709"},
"Content-Length": []string{"8963"},
"Content-Type": []string{"image/jpeg"},
"Date": []string{"Mon, 21 Jul 2025 13:24:53 GMT"},
"Etag": []string{`"78b676e65ebc31f0bb1f2f0d05098572"`},

View File

@@ -475,7 +475,7 @@ func TestGetFileWithPresignedURL(t *testing.T) { //nolint:cyclop,maintidx
"Accept-Ranges": []string{"bytes"},
"Cache-Control": []string{"max-age=30"},
"Content-Disposition": []string{`inline; filename="nhost.jpg"`},
"Content-Length": []string{"8709"},
"Content-Length": []string{"8963"},
"Content-Type": []string{"image/jpeg"},
"Date": []string{"Mon, 21 Jul 2025 13:24:53 GMT"},
"Etag": []string{`"78b676e65ebc31f0bb1f2f0d05098572"`},

View File

@@ -37,6 +37,8 @@ func mimeTypeToImageType(mimeType string) (image.ImageType, *APIError) {
return image.ImageTypeJPEG, nil
case "image/avif":
return image.ImageTypeAVIF, nil
case "image/heic", "image/heif":
return image.ImageTypeHEIC, nil
default:
return 0, BadDataError(
fmt.Errorf( //nolint: err113
@@ -73,6 +75,8 @@ func chooseImageFormat( //nolint: cyclop
return originalFormat, image.ImageTypeJPEG, nil
case api.Avif:
return originalFormat, image.ImageTypeAVIF, nil
case api.Heic:
return originalFormat, image.ImageTypeHEIC, nil
case api.Auto:
for _, acceptHeader := range acceptHeader {
acceptedTypes := strings.Split(acceptHeader, ",")
@@ -85,6 +89,8 @@ func chooseImageFormat( //nolint: cyclop
return originalFormat, image.ImageTypeJPEG, nil
case slices.Contains(acceptedTypes, "image/png"):
return originalFormat, image.ImageTypePNG, nil
case slices.Contains(acceptedTypes, "image/heic"):
return originalFormat, image.ImageTypeHEIC, nil
}
}
@@ -92,8 +98,11 @@ func chooseImageFormat( //nolint: cyclop
default:
return 0, 0, BadDataError(
//nolint: err113
fmt.Errorf("format must be one of: same, webp, png, jpeg, avif, auto. Got: %s", format),
"format must be one of: same, webp, png, jpeg, avif, auto. Got: "+string(format),
fmt.Errorf(
"format must be one of: same, webp, png, jpeg, avif, heic, auto. Got: %s",
format,
),
"format must be one of: same, webp, png, jpeg, avif, heic, auto. Got: "+string(format),
)
}
}

View File

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

View File

@@ -1,14 +1,11 @@
package image
import (
"bytes"
"fmt"
"io"
"math"
"sync"
"sync/atomic"
"github.com/davidbyttow/govips/v2/vips"
"github.com/cshum/vipsgen/vips"
)
const (
@@ -24,6 +21,7 @@ const (
ImageTypePNG
ImageTypeWEBP
ImageTypeAVIF
ImageTypeHEIC
)
type Options struct {
@@ -54,6 +52,8 @@ func (o Options) FormatMimeType() string {
return "image/webp"
case ImageTypeAVIF:
return "image/avif"
case ImageTypeHEIC:
return "image/heic"
}
return ""
@@ -69,6 +69,8 @@ func (o Options) FileExtension() string {
return "webp"
case ImageTypeAVIF:
return "avif"
case ImageTypeHEIC:
return "heic"
}
return ""
@@ -76,12 +78,10 @@ func (o Options) FileExtension() string {
type Transformer struct {
workers chan struct{}
pool sync.Pool
}
func NewTransformer() *Transformer {
if atomic.CompareAndSwapInt32(&initialized, 0, 1) {
vips.LoggingSettings(nil, vips.LogLevelWarning)
vips.Startup(nil)
}
@@ -92,11 +92,6 @@ func NewTransformer() *Transformer {
return &Transformer{
workers: workers,
pool: sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
},
}
}
@@ -104,35 +99,67 @@ func (t *Transformer) Shutdown() {
vips.Shutdown()
}
func export(image *vips.ImageRef, opts Options) ([]byte, error) {
var (
b []byte
err error
)
switch opts.Format {
case ImageTypeJPEG:
ep := vips.NewJpegExportParams()
ep.Quality = opts.Quality
b, _, err = image.ExportJpeg(ep)
case ImageTypePNG:
ep := vips.NewPngExportParams()
b, _, err = image.ExportPng(ep)
case ImageTypeWEBP:
ep := vips.NewWebpExportParams()
ep.Quality = opts.Quality
b, _, err = image.ExportWebp(ep)
case ImageTypeAVIF:
ep := vips.NewAvifExportParams()
ep.Quality = opts.Quality
ep.Effort = 0
b, _, err = image.ExportAvif(ep)
}
return b, err //nolint: wrapcheck
type readCloserAdapter struct {
io.Reader
}
func processImage(image *vips.ImageRef, opts Options) error {
func (r readCloserAdapter) Close() error {
if closer, ok := r.Reader.(io.Closer); ok {
return closer.Close() //nolint: wrapcheck
}
return nil
}
type writeCloserAdapter struct {
io.Writer
}
func (w writeCloserAdapter) Close() error {
if closer, ok := w.Writer.(io.Closer); ok {
return closer.Close() //nolint: wrapcheck
}
return nil
}
func exportToTarget(image *vips.Image, target *vips.Target, opts Options) error {
quality := opts.Quality
if quality == 0 {
quality = 75
}
var err error
switch opts.Format {
case ImageTypeJPEG:
jpegOpts := vips.DefaultJpegsaveTargetOptions()
jpegOpts.Q = quality
err = image.JpegsaveTarget(target, jpegOpts)
case ImageTypePNG:
pngOpts := vips.DefaultPngsaveTargetOptions()
err = image.PngsaveTarget(target, pngOpts)
case ImageTypeWEBP:
webpOpts := vips.DefaultWebpsaveTargetOptions()
webpOpts.Q = quality
err = image.WebpsaveTarget(target, webpOpts)
case ImageTypeAVIF:
heifOpts := vips.DefaultHeifsaveTargetOptions()
heifOpts.Q = quality
heifOpts.Compression = vips.HeifCompressionAv1
err = image.HeifsaveTarget(target, heifOpts)
case ImageTypeHEIC:
heifOpts := vips.DefaultHeifsaveTargetOptions()
heifOpts.Q = quality
heifOpts.Compression = vips.HeifCompressionHevc
err = image.HeifsaveTarget(target, heifOpts)
default:
return fmt.Errorf("unsupported format: %d", opts.Format) //nolint: err113
}
return err //nolint: wrapcheck
}
func imageResize(image *vips.Image, opts Options) error {
if opts.Width > 0 || opts.Height > 0 {
width := opts.Width
height := opts.Height
@@ -145,13 +172,33 @@ func processImage(image *vips.ImageRef, opts Options) error {
height = int((float64(width) / float64(image.Width())) * float64(image.Height()))
}
if err := image.Thumbnail(width, height, vips.InterestingCentre); err != nil {
thumbnailOpts := vips.DefaultThumbnailImageOptions()
thumbnailOpts.Crop = vips.InterestingCentre
thumbnailOpts.Height = height
if err := image.ThumbnailImage(width, thumbnailOpts); err != nil {
return fmt.Errorf("failed to thumbnail: %w", err)
}
}
return nil
}
func imagePipeline(image *vips.Image, opts Options) error {
if err := imageResize(image, opts); err != nil {
return err
}
// Auto-rotate when converting formats to ensure correct display
if opts.FormatChanged() {
if err := image.Autorot(nil); err != nil {
return fmt.Errorf("failed to auto-rotate: %w", err)
}
}
// Apply blur if specified
if opts.Blur > 0 {
if err := image.GaussianBlur(float64(opts.Blur)); err != nil {
if err := image.Gaussblur(float64(opts.Blur), nil); err != nil {
return fmt.Errorf("failed to blur: %w", err)
}
}
@@ -161,49 +208,37 @@ func processImage(image *vips.ImageRef, opts Options) error {
func (t *Transformer) Run(
orig io.Reader,
length uint64,
length uint64, //nolint:revive
modified io.Writer,
opts Options,
) error {
// this is to avoid processing too many images at the same time in order to save memory
// Limit concurrent processing to avoid processing too many images at the same time
<-t.workers
defer func() { t.workers <- struct{}{} }()
buf, _ := t.pool.Get().(*bytes.Buffer)
defer t.pool.Put(buf)
defer buf.Reset()
// Create streaming source from io.Reader
// This avoids loading the entire file into a buffer!
source := vips.NewSource(readCloserAdapter{orig})
defer source.Close()
if length > math.MaxUint32 {
panic("length is too big")
}
if l := int(length); buf.Len() < l {
buf.Grow(l)
}
_, err := io.Copy(buf, orig)
image, err := vips.NewImageFromSource(source, nil)
if err != nil {
panic(err)
return fmt.Errorf("failed to load image from source: %w", err)
}
image, err := vips.NewImageFromBuffer(buf.Bytes())
if err != nil {
return fmt.Errorf("failed to load image: %w", err)
}
defer image.Close()
if err := processImage(image, opts); err != nil {
// Apply additional processing (auto-rotate, blur)
if err := imagePipeline(image, opts); err != nil {
return err
}
b, err := export(image, opts)
if err != nil {
return fmt.Errorf("failed to export: %w", err)
}
// Export with streaming target
target := vips.NewTarget(writeCloserAdapter{modified})
defer target.Close()
if _, err = modified.Write(b); err != nil {
return fmt.Errorf("failed to write: %w", err)
if err := exportToTarget(image, target, opts); err != nil {
return fmt.Errorf("failed to export: %w", err)
}
return nil

View File

@@ -24,7 +24,7 @@ func TestManipulate(t *testing.T) {
{
name: "jpg",
filename: "testdata/nhost.jpg",
sum: "0a3f081bd296958cfc99297ee9fb4cc2e5a1b22bebfded81c6e00e25caf84f36",
sum: "3cffeb9c31e624970f6b6e440213a9805c4ecf56052268631d1add81f9fde37b",
size: 33399,
options: image.Options{
Height: 100,
@@ -37,45 +37,78 @@ func TestManipulate(t *testing.T) {
{
name: "jpg",
filename: "testdata/nhost.jpg",
sum: "cd9857da2c40b6c46d39abd0f18def2c9879c2a06a1702c131c17b2bfdb43268",
sum: "131c9a8f0dfb0aa345bb498a314da97711b5f3c146e572c02d62d09ace22db7b",
size: 33399,
options: image.Options{Width: 300, Height: 100, Blur: 2, Format: image.ImageTypeJPEG},
},
{
name: "png",
filename: "testdata/nhost.png",
sum: "d538212aa74ad1d17261bc2126e60964e6d2dc1c7898ea3b9f9bd3b5bc94b380",
sum: "ac7da45c3a994e50fdbc25123992b31116d32f20dac2c5436d2d6fdbfd319853",
size: 68307,
options: image.Options{Width: 300, Height: 100, Blur: 2, Format: image.ImageTypePNG},
},
{
name: "webp",
filename: "testdata/nhost.webp",
sum: "720eebe382c26b5fb8abf8552f282317074a4c9f6467aa8a60bb93a20f55e063",
sum: "e9e2342f901aa447ebd32ee4ef5a6f89f007f8d692350ec45b7f02b727cc043b",
size: 17784,
options: image.Options{Width: 300, Height: 100, Blur: 2, Format: image.ImageTypeWEBP},
},
{
name: "jpg only blur",
filename: "testdata/nhost.jpg",
sum: "0b038c3afbe8a848974874aad4fb11983d45ab84d9113eb4260c4ed34d2d03d6",
sum: "e359c19b3a708cfce10577d7b67f7372ddc57b478dcfc5c34b7d49e63bd13a86",
size: 33399,
options: image.Options{Blur: 2, Format: image.ImageTypeJPEG},
},
{
name: "webp to avif",
filename: "testdata/nhost.webp",
sum: "e0a5fb177567987b16b379ce2f263cc319c4f7f5e7145c9ae81682b02c7a9f6d",
sum: "44ae1c37353bcd8db71df35120be7c6c22435d258ccb3248662bd4fd181b7cf0",
size: 17784,
options: image.Options{Width: 300, Height: 100, Blur: 2, Format: image.ImageTypeAVIF},
},
{
name: "jpeg to avif, no image manipulation",
filename: "testdata/nhost.jpg",
sum: "cd3f0137250dcc145ee9f1e63733c30bccfc6ad1c058b2be719ea9e8148029db",
sum: "3c03519a14713701db1eaab77dae305b5484f20baacac9625294dd6952446062",
size: 17784,
options: image.Options{Format: image.ImageTypeAVIF},
},
{
name: "heic",
filename: "testdata/nhost.heic",
sum: "621a8cac7292d02342e699b11db69b1b5b6f55cb09ce06006a53f9cff7727a98",
size: 12968,
options: image.Options{
Width: 300,
Height: 100,
Blur: 2,
Format: image.ImageTypeHEIC,
Quality: 50,
},
},
{
name: "jpeg to heic",
filename: "testdata/nhost.jpg",
sum: "c564a40516e3c6f39a8b5d1ba934f3007512d9766630fd512a4c268a0c0b4cb8",
size: 33399,
options: image.Options{
Width: 300,
Height: 100,
Blur: 2,
Format: image.ImageTypeHEIC,
Quality: 50,
},
},
{
name: "webp to heic",
filename: "testdata/nhost.webp",
sum: "f1d3ebb85e83f70b4d283b30e5c2f765a9f8847b7dceae3d40907aab16b93004",
size: 17784,
options: image.Options{Width: 300, Height: 100, Blur: 2, Format: image.ImageTypeHEIC},
},
}
transformer := image.NewTransformer()

Binary file not shown.

21
vendor/github.com/cshum/vipsgen/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2025 Adrian Shum and contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

100
vendor/github.com/cshum/vipsgen/pointer/pointer.go generated vendored Normal file
View File

@@ -0,0 +1,100 @@
package pointer
// #include <stdlib.h>
import "C"
import (
"sync"
"unsafe"
)
const blockSize = 1024
var (
mutex sync.RWMutex
store = map[unsafe.Pointer]interface{}{}
free []unsafe.Pointer
blocks []unsafe.Pointer
)
func allocMem() {
mem := C.malloc(blockSize)
if mem == nil {
panic("can't allocate memory block for C pointers")
}
blocks = append(blocks, mem)
for i := 0; i < blockSize; i++ {
p := unsafe.Pointer(uintptr(mem) + uintptr(blockSize-1-i))
free = append(free, p)
}
}
func getPtr() unsafe.Pointer {
// Generate real fake C pointer.
// This pointer will not store any data, but will be used for indexing
// purposes. Since Go doesn't allow to cast dangling pointer to
// unsafe.Pointer, we do really allocate memory. Why we need indexing? Because
// Go doest allow C code to store pointers to Go data.
if len(free) == 0 {
allocMem()
}
n := len(free) - 1
p := free[n]
free = free[:n]
return p
}
// Save an object in the storage and return an index pointer to it.
func Save(v interface{}) unsafe.Pointer {
if v == nil {
return nil
}
mutex.Lock()
ptr := getPtr()
store[ptr] = v
mutex.Unlock()
return ptr
}
// Restore an object from the storage by its index pointer.
func Restore(ptr unsafe.Pointer) (v interface{}) {
if ptr == nil {
return nil
}
mutex.RLock()
v = store[ptr]
mutex.RUnlock()
return
}
// Unref removes an object from the storage and returns the index pointer to the
// pool for reuse.
func Unref(ptr unsafe.Pointer) {
if ptr == nil {
return
}
mutex.Lock()
if _, ok := store[ptr]; ok {
delete(store, ptr)
free = append(free, ptr)
}
mutex.Unlock()
}
// Clear storage and free all memory
func Clear() {
mutex.Lock()
for p := range store {
delete(store, p)
}
free = nil
for _, p := range blocks {
C.free(p)
}
blocks = nil
mutex.Unlock()
}

76
vendor/github.com/cshum/vipsgen/vips/callback.go generated vendored Normal file
View File

@@ -0,0 +1,76 @@
// Code generated by github.com/cshum/vipsgen from libvips 8.17.2; DO NOT EDIT.
package vips
import "C"
import (
"github.com/cshum/vipsgen/pointer"
"io"
"reflect"
"unsafe"
)
//export goLoggingHandler
func goLoggingHandler(domain *C.char, level C.int, message *C.char) {
log(C.GoString(domain), LogLevel(level), C.GoString(message))
}
//export goSourceRead
func goSourceRead(
ptr unsafe.Pointer, buffer unsafe.Pointer, size C.longlong,
) C.longlong {
src, ok := pointer.Restore(ptr).(*Source)
if !ok {
return -1
}
sh := &reflect.SliceHeader{
Data: uintptr(buffer),
Len: int(size),
Cap: int(size),
}
buf := *(*[]byte)(unsafe.Pointer(sh))
n, err := src.reader.Read(buf)
if err == io.EOF {
return C.longlong(n)
} else if err != nil {
return -1
}
return C.longlong(n)
}
//export goSourceSeek
func goSourceSeek(
ptr unsafe.Pointer, offset C.longlong, whence int,
) C.longlong {
src, ok := pointer.Restore(ptr).(*Source)
if ok && src.seeker != nil {
switch whence {
case io.SeekStart, io.SeekCurrent, io.SeekEnd:
if n, err := src.seeker.Seek(int64(offset), whence); err == nil {
return C.longlong(n)
}
}
}
return -1
}
//export goTargetWrite
func goTargetWrite(
ptr unsafe.Pointer, buffer unsafe.Pointer, size C.longlong,
) C.longlong {
target, ok := pointer.Restore(ptr).(*Target)
if !ok {
return -1
}
sh := &reflect.SliceHeader{
Data: uintptr(buffer),
Len: int(size),
Cap: int(size),
}
buf := *(*[]byte)(unsafe.Pointer(sh))
n, err := target.writer.Write(buf)
if err != nil {
return -1
}
return C.longlong(n)
}

48
vendor/github.com/cshum/vipsgen/vips/connection.c generated vendored Normal file
View File

@@ -0,0 +1,48 @@
// Code generated by github.com/cshum/vipsgen from libvips 8.17.2; DO NOT EDIT.
#include "connection.h"
static gint64 go_read(VipsSourceCustom *source_custom, void *buffer, gint64 length, void* ptr)
{
return goSourceRead(ptr, buffer, length);
}
static gint64 go_seek(VipsSourceCustom *source_custom, gint64 offset, int whence, void* ptr)
{
return goSourceSeek(ptr, offset, whence);
}
static gint64 go_write(VipsTargetCustom *target_custom, void *buffer, gint64 length, void* ptr)
{
return goTargetWrite(ptr, buffer, length);
}
VipsSourceCustom * create_go_custom_source(void* ptr)
{
VipsSourceCustom * source_custom = vips_source_custom_new();
g_signal_connect(source_custom, "read", G_CALLBACK(go_read), ptr);
return source_custom;
}
VipsSourceCustom * create_go_custom_source_with_seek(void* ptr)
{
VipsSourceCustom * source_custom = vips_source_custom_new();
g_signal_connect(source_custom, "read", G_CALLBACK(go_read), ptr);
g_signal_connect(source_custom, "seek", G_CALLBACK(go_seek), ptr);
return source_custom;
}
VipsTargetCustom * create_go_custom_target(void* ptr)
{
VipsTargetCustom * target_custom = vips_target_custom_new();
g_signal_connect(target_custom, "write", G_CALLBACK(go_write), ptr);
return target_custom;
}
void clear_source(VipsSourceCustom **source_custom) {
if (G_IS_OBJECT(*source_custom)) g_clear_object(source_custom);
}
void clear_target(VipsTargetCustom **target_custom) {
if (G_IS_OBJECT(*target_custom)) g_clear_object(target_custom);
}

97
vendor/github.com/cshum/vipsgen/vips/connection.go generated vendored Normal file
View File

@@ -0,0 +1,97 @@
// Code generated by github.com/cshum/vipsgen from libvips 8.17.2; DO NOT EDIT.
package vips
// #include "connection.h"
import "C"
import (
"fmt"
"github.com/cshum/vipsgen/pointer"
"io"
"sync"
"unsafe"
)
// Source contains a libvips VipsSourceCustom and manages its lifecycle.
type Source struct {
reader io.ReadCloser
seeker io.Seeker
src *C.VipsSourceCustom
ptr unsafe.Pointer
lock sync.Mutex
}
// NewSource creates Source from reader
func NewSource(reader io.ReadCloser) *Source {
Startup(nil)
s := &Source{reader: reader}
seeker, ok := reader.(io.ReadSeeker)
if ok {
s.seeker = seeker
s.ptr = pointer.Save(s)
s.src = C.create_go_custom_source_with_seek(s.ptr)
} else {
s.ptr = pointer.Save(s)
s.src = C.create_go_custom_source(s.ptr)
}
return s
}
// Close source
func (s *Source) Close() {
if s == nil {
return
}
s.lock.Lock()
if s.ptr != nil {
C.clear_source(&s.src)
pointer.Unref(s.ptr)
s.ptr = nil
s.lock.Unlock()
if s.reader != nil {
_ = s.reader.Close()
s.reader = nil
}
log("vipsgen", LogLevelDebug, fmt.Sprintf("closing source %p", s))
} else {
s.lock.Unlock()
}
}
// Target contains a libvips VipsTargetCustom and manages its lifecycle.
type Target struct {
writer io.WriteCloser
target *C.VipsTargetCustom
ptr unsafe.Pointer
lock sync.Mutex
}
// NewTarget creates Target from writer
func NewTarget(writer io.WriteCloser) *Target {
Startup(nil)
t := &Target{writer: writer}
t.ptr = pointer.Save(t)
t.target = C.create_go_custom_target(t.ptr)
return t
}
// Close target
func (t *Target) Close() {
if t == nil {
return
}
t.lock.Lock()
if t.ptr != nil {
C.clear_target(&t.target)
pointer.Unref(t.ptr)
t.ptr = nil
t.lock.Unlock()
if t.writer != nil {
_ = t.writer.Close()
t.writer = nil
}
log("vipsgen", LogLevelDebug, fmt.Sprintf("closing target %p", t))
} else {
t.lock.Unlock()
}
}

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