Compare commits
11 Commits
feat/cron-
...
feat/modif
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1060cc2313 | ||
|
|
e79d81cb68 | ||
|
|
0c63480cc7 | ||
|
|
ab890d8593 | ||
|
|
ee2d9763f7 | ||
|
|
f7ea20db61 | ||
|
|
99ac1aee3a | ||
|
|
bb9aaf2903 | ||
|
|
8e82edd0c6 | ||
|
|
b0256da33f | ||
|
|
351daa5fbe |
@@ -49,4 +49,4 @@ This repository is a monorepo that contains multiple packages and applications.
|
||||
- `tools/codegen` - Internal code generation tool to build the SDK
|
||||
- `tools/mintlify-openapi` - Internal tool to generate reference documentation for Mintlify from an OpenAPI spec.
|
||||
|
||||
For details about those projects and how to contribure, please refer to their respective `README.md` and `CONTRIBUTING.md` files.
|
||||
For details about those projects and how to contribute, please refer to their respective `README.md` and `CONTRIBUTING.md` files.
|
||||
|
||||
@@ -107,6 +107,7 @@ Nhost is frontend agnostic, which means Nhost works with all frontend frameworks
|
||||
# Resources
|
||||
|
||||
- Start developing locally with the [Nhost CLI](https://docs.nhost.io/platform/cli/local-development)
|
||||
|
||||
## Nhost Clients
|
||||
|
||||
- [JavaScript/TypeScript](https://docs.nhost.io/reference/javascript/nhost-js/main)
|
||||
@@ -137,7 +138,7 @@ Here are some ways of contributing to making Nhost better:
|
||||
|
||||
- **[Try out Nhost](https://docs.nhost.io)**, and think of ways to make the service better. Let us know here on GitHub.
|
||||
- Join our [Discord](https://discord.com/invite/9V7Qb2U) and connect with other members to share and learn from.
|
||||
- Send a pull request to any of our [open source repositories](https://github.com/nhost) on Github. Check our [contribution guide](https://github.com/nhost/nhost/blob/main/CONTRIBUTING.md) and our [developers guide](https://github.com/nhost/nhost/blob/main/DEVELOPERS.md) for more details about how to contribute. We're looking forward to your contribution!
|
||||
- Send a pull request to any of our [open source repositories](https://github.com/nhost) on Github. Check out our [contribution guide](https://github.com/nhost/nhost/blob/main/CONTRIBUTING.md) for more details about how to contribute. We're looking forward to your contribution!
|
||||
|
||||
### Contributors
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -250,7 +250,7 @@ func traefik(subdomain, projectName string, port uint, dotnhostfolder string) (*
|
||||
}
|
||||
|
||||
return &Service{
|
||||
Image: "traefik:v3.1",
|
||||
Image: "traefik:v3.6",
|
||||
DependsOn: nil,
|
||||
EntryPoint: nil,
|
||||
Command: []string{
|
||||
|
||||
@@ -4191,32 +4191,34 @@ type Plans struct {
|
||||
// An array relationship
|
||||
Organizations []*Organizations `json:"organizations"`
|
||||
Price int64 `json:"price"`
|
||||
SLALevel SLALevelEnum `json:"slaLevel"`
|
||||
Sort int64 `json:"sort"`
|
||||
UpatedAt time.Time `json:"upatedAt"`
|
||||
}
|
||||
|
||||
// Boolean expression to filter rows from the table "plans". All fields are combined with a logical 'AND'.
|
||||
type PlansBoolExp struct {
|
||||
And []*PlansBoolExp `json:"_and,omitempty"`
|
||||
Not *PlansBoolExp `json:"_not,omitempty"`
|
||||
Or []*PlansBoolExp `json:"_or,omitempty"`
|
||||
Apps *AppsBoolExp `json:"apps,omitempty"`
|
||||
CreatedAt *TimestamptzComparisonExp `json:"createdAt,omitempty"`
|
||||
Deprecated *BooleanComparisonExp `json:"deprecated,omitempty"`
|
||||
FeatureBackupEnabled *BooleanComparisonExp `json:"featureBackupEnabled,omitempty"`
|
||||
FeatureCustomDomainsEnabled *BooleanComparisonExp `json:"featureCustomDomainsEnabled,omitempty"`
|
||||
FeatureCustomEmailTemplatesEnabled *BooleanComparisonExp `json:"featureCustomEmailTemplatesEnabled,omitempty"`
|
||||
FeatureMaxDbSize *IntComparisonExp `json:"featureMaxDbSize,omitempty"`
|
||||
ID *UUIDComparisonExp `json:"id,omitempty"`
|
||||
Individual *BooleanComparisonExp `json:"individual,omitempty"`
|
||||
IsDefault *BooleanComparisonExp `json:"isDefault,omitempty"`
|
||||
IsFree *BooleanComparisonExp `json:"isFree,omitempty"`
|
||||
IsPublic *BooleanComparisonExp `json:"isPublic,omitempty"`
|
||||
Name *StringComparisonExp `json:"name,omitempty"`
|
||||
Organizations *OrganizationsBoolExp `json:"organizations,omitempty"`
|
||||
Price *IntComparisonExp `json:"price,omitempty"`
|
||||
Sort *IntComparisonExp `json:"sort,omitempty"`
|
||||
UpatedAt *TimestamptzComparisonExp `json:"upatedAt,omitempty"`
|
||||
And []*PlansBoolExp `json:"_and,omitempty"`
|
||||
Not *PlansBoolExp `json:"_not,omitempty"`
|
||||
Or []*PlansBoolExp `json:"_or,omitempty"`
|
||||
Apps *AppsBoolExp `json:"apps,omitempty"`
|
||||
CreatedAt *TimestamptzComparisonExp `json:"createdAt,omitempty"`
|
||||
Deprecated *BooleanComparisonExp `json:"deprecated,omitempty"`
|
||||
FeatureBackupEnabled *BooleanComparisonExp `json:"featureBackupEnabled,omitempty"`
|
||||
FeatureCustomDomainsEnabled *BooleanComparisonExp `json:"featureCustomDomainsEnabled,omitempty"`
|
||||
FeatureCustomEmailTemplatesEnabled *BooleanComparisonExp `json:"featureCustomEmailTemplatesEnabled,omitempty"`
|
||||
FeatureMaxDbSize *IntComparisonExp `json:"featureMaxDbSize,omitempty"`
|
||||
ID *UUIDComparisonExp `json:"id,omitempty"`
|
||||
Individual *BooleanComparisonExp `json:"individual,omitempty"`
|
||||
IsDefault *BooleanComparisonExp `json:"isDefault,omitempty"`
|
||||
IsFree *BooleanComparisonExp `json:"isFree,omitempty"`
|
||||
IsPublic *BooleanComparisonExp `json:"isPublic,omitempty"`
|
||||
Name *StringComparisonExp `json:"name,omitempty"`
|
||||
Organizations *OrganizationsBoolExp `json:"organizations,omitempty"`
|
||||
Price *IntComparisonExp `json:"price,omitempty"`
|
||||
SLALevel *SLALevelEnumComparisonExp `json:"slaLevel,omitempty"`
|
||||
Sort *IntComparisonExp `json:"sort,omitempty"`
|
||||
UpatedAt *TimestamptzComparisonExp `json:"upatedAt,omitempty"`
|
||||
}
|
||||
|
||||
// Ordering options when selecting data from "plans".
|
||||
@@ -4236,6 +4238,7 @@ type PlansOrderBy struct {
|
||||
Name *OrderBy `json:"name,omitempty"`
|
||||
OrganizationsAggregate *OrganizationsAggregateOrderBy `json:"organizations_aggregate,omitempty"`
|
||||
Price *OrderBy `json:"price,omitempty"`
|
||||
SLALevel *OrderBy `json:"slaLevel,omitempty"`
|
||||
Sort *OrderBy `json:"sort,omitempty"`
|
||||
UpatedAt *OrderBy `json:"upatedAt,omitempty"`
|
||||
}
|
||||
@@ -4250,21 +4253,22 @@ type PlansStreamCursorInput struct {
|
||||
|
||||
// Initial value of the column from where the streaming should start
|
||||
type PlansStreamCursorValueInput struct {
|
||||
CreatedAt *time.Time `json:"createdAt,omitempty"`
|
||||
Deprecated *bool `json:"deprecated,omitempty"`
|
||||
FeatureBackupEnabled *bool `json:"featureBackupEnabled,omitempty"`
|
||||
FeatureCustomDomainsEnabled *bool `json:"featureCustomDomainsEnabled,omitempty"`
|
||||
FeatureCustomEmailTemplatesEnabled *bool `json:"featureCustomEmailTemplatesEnabled,omitempty"`
|
||||
FeatureMaxDbSize *int64 `json:"featureMaxDbSize,omitempty"`
|
||||
ID *string `json:"id,omitempty"`
|
||||
Individual *bool `json:"individual,omitempty"`
|
||||
IsDefault *bool `json:"isDefault,omitempty"`
|
||||
IsFree *bool `json:"isFree,omitempty"`
|
||||
IsPublic *bool `json:"isPublic,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Price *int64 `json:"price,omitempty"`
|
||||
Sort *int64 `json:"sort,omitempty"`
|
||||
UpatedAt *time.Time `json:"upatedAt,omitempty"`
|
||||
CreatedAt *time.Time `json:"createdAt,omitempty"`
|
||||
Deprecated *bool `json:"deprecated,omitempty"`
|
||||
FeatureBackupEnabled *bool `json:"featureBackupEnabled,omitempty"`
|
||||
FeatureCustomDomainsEnabled *bool `json:"featureCustomDomainsEnabled,omitempty"`
|
||||
FeatureCustomEmailTemplatesEnabled *bool `json:"featureCustomEmailTemplatesEnabled,omitempty"`
|
||||
FeatureMaxDbSize *int64 `json:"featureMaxDbSize,omitempty"`
|
||||
ID *string `json:"id,omitempty"`
|
||||
Individual *bool `json:"individual,omitempty"`
|
||||
IsDefault *bool `json:"isDefault,omitempty"`
|
||||
IsFree *bool `json:"isFree,omitempty"`
|
||||
IsPublic *bool `json:"isPublic,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Price *int64 `json:"price,omitempty"`
|
||||
SLALevel *SLALevelEnum `json:"slaLevel,omitempty"`
|
||||
Sort *int64 `json:"sort,omitempty"`
|
||||
UpatedAt *time.Time `json:"upatedAt,omitempty"`
|
||||
}
|
||||
|
||||
type QueryRoot struct {
|
||||
@@ -4694,6 +4698,15 @@ type RunServiceStreamCursorValueInput struct {
|
||||
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
|
||||
}
|
||||
|
||||
// Boolean expression to compare columns of type "sla_level_enum". All fields are combined with logical 'AND'.
|
||||
type SLALevelEnumComparisonExp struct {
|
||||
Eq *SLALevelEnum `json:"_eq,omitempty"`
|
||||
In []SLALevelEnum `json:"_in,omitempty"`
|
||||
IsNull *bool `json:"_is_null,omitempty"`
|
||||
Neq *SLALevelEnum `json:"_neq,omitempty"`
|
||||
Nin []SLALevelEnum `json:"_nin,omitempty"`
|
||||
}
|
||||
|
||||
// Boolean expression to compare columns of type "software_type_enum". All fields are combined with logical 'AND'.
|
||||
type SoftwareTypeEnumComparisonExp struct {
|
||||
Eq *SoftwareTypeEnum `json:"_eq,omitempty"`
|
||||
@@ -8542,6 +8555,8 @@ const (
|
||||
// column name
|
||||
PlansSelectColumnPrice PlansSelectColumn = "price"
|
||||
// column name
|
||||
PlansSelectColumnSLALevel PlansSelectColumn = "slaLevel"
|
||||
// column name
|
||||
PlansSelectColumnSort PlansSelectColumn = "sort"
|
||||
// column name
|
||||
PlansSelectColumnUpatedAt PlansSelectColumn = "upatedAt"
|
||||
@@ -8561,13 +8576,14 @@ var AllPlansSelectColumn = []PlansSelectColumn{
|
||||
PlansSelectColumnIsPublic,
|
||||
PlansSelectColumnName,
|
||||
PlansSelectColumnPrice,
|
||||
PlansSelectColumnSLALevel,
|
||||
PlansSelectColumnSort,
|
||||
PlansSelectColumnUpatedAt,
|
||||
}
|
||||
|
||||
func (e PlansSelectColumn) IsValid() bool {
|
||||
switch e {
|
||||
case PlansSelectColumnCreatedAt, PlansSelectColumnDeprecated, PlansSelectColumnFeatureBackupEnabled, PlansSelectColumnFeatureCustomDomainsEnabled, PlansSelectColumnFeatureCustomEmailTemplatesEnabled, PlansSelectColumnFeatureMaxDbSize, PlansSelectColumnID, PlansSelectColumnIndividual, PlansSelectColumnIsDefault, PlansSelectColumnIsFree, PlansSelectColumnIsPublic, PlansSelectColumnName, PlansSelectColumnPrice, PlansSelectColumnSort, PlansSelectColumnUpatedAt:
|
||||
case PlansSelectColumnCreatedAt, PlansSelectColumnDeprecated, PlansSelectColumnFeatureBackupEnabled, PlansSelectColumnFeatureCustomDomainsEnabled, PlansSelectColumnFeatureCustomEmailTemplatesEnabled, PlansSelectColumnFeatureMaxDbSize, PlansSelectColumnID, PlansSelectColumnIndividual, PlansSelectColumnIsDefault, PlansSelectColumnIsFree, PlansSelectColumnIsPublic, PlansSelectColumnName, PlansSelectColumnPrice, PlansSelectColumnSLALevel, PlansSelectColumnSort, PlansSelectColumnUpatedAt:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -8954,6 +8970,66 @@ func (e RunServiceSelectColumn) MarshalJSON() ([]byte, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type SLALevelEnum string
|
||||
|
||||
const (
|
||||
// No SLA
|
||||
SLALevelEnumNone SLALevelEnum = "none"
|
||||
// Premium SLA
|
||||
SLALevelEnumPremium SLALevelEnum = "premium"
|
||||
// Standard SLA
|
||||
SLALevelEnumStandard SLALevelEnum = "standard"
|
||||
)
|
||||
|
||||
var AllSLALevelEnum = []SLALevelEnum{
|
||||
SLALevelEnumNone,
|
||||
SLALevelEnumPremium,
|
||||
SLALevelEnumStandard,
|
||||
}
|
||||
|
||||
func (e SLALevelEnum) IsValid() bool {
|
||||
switch e {
|
||||
case SLALevelEnumNone, SLALevelEnumPremium, SLALevelEnumStandard:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e SLALevelEnum) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *SLALevelEnum) UnmarshalGQL(v any) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = SLALevelEnum(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid sla_level_enum", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e SLALevelEnum) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
func (e *SLALevelEnum) UnmarshalJSON(b []byte) error {
|
||||
s, err := strconv.Unquote(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.UnmarshalGQL(s)
|
||||
}
|
||||
|
||||
func (e SLALevelEnum) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
e.MarshalGQL(&buf)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type SoftwareTypeEnum string
|
||||
|
||||
const (
|
||||
|
||||
@@ -233,7 +233,8 @@
|
||||
},
|
||||
"overrides": {
|
||||
"esbuild@<=0.24.2": ">=0.25.0",
|
||||
"js-yaml@<=4.1.0": ">=4.1.1"
|
||||
"js-yaml@<=4.1.0": ">=4.1.1",
|
||||
"glob@>=10.3.7 <=11.0.3": ">=11.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
99
dashboard/pnpm-lock.yaml
generated
99
dashboard/pnpm-lock.yaml
generated
@@ -7,6 +7,7 @@ settings:
|
||||
overrides:
|
||||
esbuild@<=0.24.2: '>=0.25.0'
|
||||
js-yaml@<=4.1.0: '>=4.1.1'
|
||||
glob@>=10.3.7 <=11.0.3: '>=11.1.0'
|
||||
|
||||
packageExtensionsChecksum: sha256-gRFeykwiwMfEE6etcYx6N48XwVeKzxbqNveL7KTQgSQ=
|
||||
|
||||
@@ -2246,6 +2247,14 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@isaacs/balanced-match@4.0.1':
|
||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2619,10 +2628,6 @@ packages:
|
||||
'@orval/zod@7.11.2':
|
||||
resolution: {integrity: sha512-4MzTg5Wms8/LlM3CbYu80dvCbP88bVlQjnYsBdFXuEv0K2GYkBCAhVOrmXCVrPXE89neV6ABkvWQeuKZQpkdxQ==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@playwright/test@1.54.1':
|
||||
resolution: {integrity: sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -5650,8 +5655,8 @@ packages:
|
||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
foreground-child@3.1.1:
|
||||
resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.4:
|
||||
@@ -5780,8 +5785,9 @@ packages:
|
||||
glob-to-regexp@0.4.1:
|
||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||
|
||||
glob@10.4.5:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
glob@12.0.0:
|
||||
resolution: {integrity: sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==}
|
||||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
glob@7.1.7:
|
||||
@@ -6289,9 +6295,9 @@ packages:
|
||||
iterator.prototype@1.1.2:
|
||||
resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==}
|
||||
|
||||
jackspeak@3.2.3:
|
||||
resolution: {integrity: sha512-htOzIMPbpLid/Gq9/zaz9SfExABxqRe1sSCdxntlO/aMD6u0issZQiY25n2GKQUtJ02j7z5sfptlAOMpWWOmvw==}
|
||||
engines: {node: '>=14'}
|
||||
jackspeak@4.1.1:
|
||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
jest-diff@29.7.0:
|
||||
resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==}
|
||||
@@ -6556,9 +6562,9 @@ packages:
|
||||
lowlight@3.1.0:
|
||||
resolution: {integrity: sha512-CEbNVoSikAxwDMDPjXlqlFYiZLkDJHwyGu/MfOsJnF3d7f3tds5J3z8s/l9TMXhzfsJCCJEAsD78842mwmg0PQ==}
|
||||
|
||||
lru-cache@10.2.2:
|
||||
resolution: {integrity: sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==}
|
||||
engines: {node: 14 || >=16.14}
|
||||
lru-cache@11.2.2:
|
||||
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
@@ -6799,6 +6805,10 @@ packages:
|
||||
resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
|
||||
hasBin: true
|
||||
|
||||
minimatch@10.1.1:
|
||||
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
@@ -6810,10 +6820,6 @@ packages:
|
||||
resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minimatch@9.0.4:
|
||||
resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minimatch@9.0.5:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -7182,9 +7188,9 @@ packages:
|
||||
resolution: {integrity: sha512-QLcPegTHF11axjfojBIoDygmS2E3Lf+8+jI6wOVmNVenrKSo3mFdSGiIgdSHenczw3wPtlVMQaFVwGmM7BJdtg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
path-scurry@2.0.1:
|
||||
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
path-to-regexp@6.3.0:
|
||||
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
||||
@@ -11153,11 +11159,17 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.14.8
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
dependencies:
|
||||
'@isaacs/balanced-match': 4.0.1
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
string-width-cjs: string-width@4.2.3
|
||||
strip-ansi: 7.1.0
|
||||
strip-ansi: 7.1.2
|
||||
strip-ansi-cjs: strip-ansi@6.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
@@ -11605,9 +11617,6 @@ snapshots:
|
||||
- openapi-types
|
||||
- supports-color
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@playwright/test@1.54.1':
|
||||
dependencies:
|
||||
playwright: 1.54.1
|
||||
@@ -15242,7 +15251,7 @@ snapshots:
|
||||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
foreground-child@3.1.1:
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
@@ -15383,14 +15392,14 @@ snapshots:
|
||||
|
||||
glob-to-regexp@0.4.1: {}
|
||||
|
||||
glob@10.4.5:
|
||||
glob@12.0.0:
|
||||
dependencies:
|
||||
foreground-child: 3.1.1
|
||||
jackspeak: 3.2.3
|
||||
minimatch: 9.0.4
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 4.1.1
|
||||
minimatch: 10.1.1
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
path-scurry: 2.0.1
|
||||
|
||||
glob@7.1.7:
|
||||
dependencies:
|
||||
@@ -15913,11 +15922,9 @@ snapshots:
|
||||
reflect.getprototypeof: 1.0.8
|
||||
set-function-name: 2.0.2
|
||||
|
||||
jackspeak@3.2.3:
|
||||
jackspeak@4.1.1:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jest-diff@29.7.0:
|
||||
dependencies:
|
||||
@@ -16211,7 +16218,7 @@ snapshots:
|
||||
devlop: 1.1.0
|
||||
highlight.js: 11.9.0
|
||||
|
||||
lru-cache@10.2.2: {}
|
||||
lru-cache@11.2.2: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
@@ -16641,6 +16648,10 @@ snapshots:
|
||||
|
||||
mini-svg-data-uri@1.4.4: {}
|
||||
|
||||
minimatch@10.1.1:
|
||||
dependencies:
|
||||
'@isaacs/brace-expansion': 5.0.0
|
||||
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
@@ -16653,10 +16664,6 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@9.0.4:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@9.0.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
@@ -17095,9 +17102,9 @@ snapshots:
|
||||
dependencies:
|
||||
path-root-regex: 0.1.2
|
||||
|
||||
path-scurry@1.11.1:
|
||||
path-scurry@2.0.1:
|
||||
dependencies:
|
||||
lru-cache: 10.2.2
|
||||
lru-cache: 11.2.2
|
||||
minipass: 7.1.2
|
||||
|
||||
path-to-regexp@6.3.0: {}
|
||||
@@ -17943,7 +17950,7 @@ snapshots:
|
||||
dependencies:
|
||||
eastasianwidth: 0.2.0
|
||||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.1.0
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
string-width@7.2.0:
|
||||
dependencies:
|
||||
@@ -18062,7 +18069,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
commander: 4.1.1
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
lines-and-columns: 1.2.4
|
||||
mz: 2.7.0
|
||||
pirates: 4.0.6
|
||||
@@ -18164,7 +18171,7 @@ snapshots:
|
||||
test-exclude@7.0.1:
|
||||
dependencies:
|
||||
'@istanbuljs/schema': 0.1.3
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
minimatch: 9.0.5
|
||||
|
||||
text-table@0.2.0: {}
|
||||
@@ -18790,9 +18797,9 @@ snapshots:
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.1
|
||||
ansi-styles: 6.2.3
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.1.0
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
wrap-ansi@9.0.0:
|
||||
dependencies:
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as EditSettingsForm } from './EditSettingsForm';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as useTableIsEnumQuery } from './useTableIsEnumQuery';
|
||||
@@ -21,23 +21,22 @@ import { ReplicasFormSection } from '@/features/orgs/projects/services/component
|
||||
import { StorageFormSection } from '@/features/orgs/projects/services/components/ServiceForm/components/StorageFormSection';
|
||||
|
||||
import {
|
||||
defaultServiceFormValues,
|
||||
validationSchema,
|
||||
type Port,
|
||||
type ServiceFormProps,
|
||||
type ServiceFormValues,
|
||||
} from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
|
||||
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { getFormattedServiceConfig } from '@/features/orgs/projects/services/utils/getFormattedServiceConfig';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import {
|
||||
useInsertRunServiceConfigMutation,
|
||||
useReplaceRunServiceConfigMutation,
|
||||
type ConfigRunServiceConfigInsertInput,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import { RESOURCE_VCPU_MULTIPLIER } from '@/utils/constants/common';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { removeTypename } from '@/utils/helpers';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
@@ -69,14 +68,7 @@ export default function ServiceForm({
|
||||
useState<Error | null>(null);
|
||||
|
||||
const form = useForm<ServiceFormValues>({
|
||||
defaultValues: initialData ?? {
|
||||
compute: {
|
||||
cpu: 62,
|
||||
memory: 128,
|
||||
},
|
||||
replicas: 1,
|
||||
autoscaler: null,
|
||||
},
|
||||
defaultValues: initialData ?? defaultServiceFormValues,
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
@@ -142,66 +134,8 @@ export default function ServiceForm({
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
const getFormattedConfig = (values: ServiceFormValues) => {
|
||||
// Remove any __typename property from the values
|
||||
const sanitizedValues = removeTypename(values) as ServiceFormValues;
|
||||
const sanitizedInitialDataPorts: Port[] = initialData?.ports
|
||||
? removeTypename(initialData.ports)
|
||||
: [];
|
||||
|
||||
const config: ConfigRunServiceConfigInsertInput = {
|
||||
name: sanitizedValues.name,
|
||||
image: {
|
||||
image: sanitizedValues.image,
|
||||
pullCredentials: sanitizedValues.pullCredentials,
|
||||
},
|
||||
command: sanitizedValues.command?.map((arg) => arg.argument),
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: sanitizedValues.compute?.cpu,
|
||||
memory: sanitizedValues.compute?.memory,
|
||||
},
|
||||
storage: sanitizedValues.storage?.map((item) => ({
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
capacity: item.capacity,
|
||||
})),
|
||||
replicas: sanitizedValues.replicas,
|
||||
autoscaler: sanitizedValues.autoscaler
|
||||
? {
|
||||
maxReplicas: sanitizedValues.autoscaler?.maxReplicas,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
environment: sanitizedValues.environment?.map((item) => ({
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
})),
|
||||
ports: sanitizedValues.ports?.map((item) => ({
|
||||
port: item.port,
|
||||
type: item.type,
|
||||
publish: item.publish,
|
||||
ingresses: item.ingresses as any, // cannot be changed on the UI always null type checking can be skipped.
|
||||
rateLimit:
|
||||
sanitizedInitialDataPorts.find(
|
||||
(port) => port.port === item.port && port.type === item.type,
|
||||
)?.rateLimit ?? (null as any), // cannot be changed on the UI always null type checking can be skipped.
|
||||
})),
|
||||
healthCheck: sanitizedValues.healthCheck
|
||||
? {
|
||||
port: sanitizedValues.healthCheck?.port,
|
||||
initialDelaySeconds:
|
||||
sanitizedValues.healthCheck?.initialDelaySeconds,
|
||||
probePeriodSeconds: sanitizedValues.healthCheck?.probePeriodSeconds,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
const createOrUpdateService = async (values: ServiceFormValues) => {
|
||||
const config = getFormattedConfig(values);
|
||||
const config = getFormattedServiceConfig({ values, initialData });
|
||||
|
||||
if (serviceID) {
|
||||
// Update service config
|
||||
@@ -292,7 +226,10 @@ export default function ServiceForm({
|
||||
};
|
||||
|
||||
const copyConfig = () => {
|
||||
const config = getFormattedConfig(formValues);
|
||||
const config = getFormattedServiceConfig({
|
||||
values: formValues,
|
||||
initialData,
|
||||
});
|
||||
|
||||
const base64Config = btoa(JSON.stringify(config));
|
||||
|
||||
|
||||
@@ -87,6 +87,15 @@ export type ServiceFormInitialData = Omit<ServiceFormValues, 'ports'> & {
|
||||
}[];
|
||||
};
|
||||
|
||||
export const defaultServiceFormValues = {
|
||||
compute: {
|
||||
cpu: 62,
|
||||
memory: 128,
|
||||
},
|
||||
replicas: 1,
|
||||
autoscaler: null,
|
||||
};
|
||||
|
||||
export interface ServiceFormProps extends DialogFormProps {
|
||||
/**
|
||||
* To use in conjunction with initialData to allow for updating the service
|
||||
|
||||
@@ -15,7 +15,10 @@ import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatfo
|
||||
import { type RunService } from '@/features/orgs/projects/common/hooks/useRunServices';
|
||||
import { ServiceForm } from '@/features/orgs/projects/services/components/ServiceForm';
|
||||
import { type PortTypes } from '@/features/orgs/projects/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
|
||||
import type { ServiceFormInitialData } from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
|
||||
import {
|
||||
defaultServiceFormValues,
|
||||
type ServiceFormInitialData,
|
||||
} from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
|
||||
import { copy } from '@/utils/copy';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
@@ -74,12 +77,15 @@ export default function ServicesList({
|
||||
ingresses: item.ingresses,
|
||||
rateLimit: item.rateLimit,
|
||||
})),
|
||||
compute: service.config?.resources?.compute ?? {
|
||||
cpu: 62,
|
||||
memory: 128,
|
||||
},
|
||||
replicas: service.config?.resources?.replicas,
|
||||
autoscaler: service?.config?.resources?.autoscaler,
|
||||
compute:
|
||||
service.config?.resources?.compute ??
|
||||
defaultServiceFormValues.compute,
|
||||
replicas:
|
||||
service.config?.resources?.replicas ??
|
||||
defaultServiceFormValues.replicas,
|
||||
autoscaler:
|
||||
service?.config?.resources?.autoscaler ??
|
||||
defaultServiceFormValues.autoscaler,
|
||||
storage: service.config?.resources?.storage,
|
||||
} as ServiceFormInitialData
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { PortTypes } from '@/features/orgs/projects/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
|
||||
import getFormattedServiceConfig from './getFormattedServiceConfig';
|
||||
|
||||
describe('getFormattedServiceConfig', () => {
|
||||
it('pghero config should be formatted correctly', () => {
|
||||
const pgheroFormValues = {
|
||||
name: 'pghero',
|
||||
image: 'docker.io/ankane/pghero:latest',
|
||||
command: [],
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
storage: [],
|
||||
replicas: 1,
|
||||
},
|
||||
environment: [
|
||||
{
|
||||
name: 'DATABASE_URL',
|
||||
value:
|
||||
'postgres://postgres:[PASSWORD]@postgres-service:5432/[SUBDOMAIN]?sslmode=disable',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_USERNAME',
|
||||
value: '[USER]',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_PASSWORD',
|
||||
value: '[PASSWORD]',
|
||||
},
|
||||
],
|
||||
ports: [
|
||||
{
|
||||
port: 8080,
|
||||
type: PortTypes.HTTP,
|
||||
publish: true,
|
||||
},
|
||||
],
|
||||
autoscaler: null,
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
replicas: 1,
|
||||
storage: [],
|
||||
};
|
||||
|
||||
const formattedConfig = getFormattedServiceConfig({
|
||||
values: pgheroFormValues,
|
||||
});
|
||||
|
||||
const expected = {
|
||||
name: 'pghero',
|
||||
image: {
|
||||
image: 'docker.io/ankane/pghero:latest',
|
||||
},
|
||||
command: [],
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
storage: [],
|
||||
replicas: 1,
|
||||
autoscaler: null,
|
||||
},
|
||||
environment: [
|
||||
{
|
||||
name: 'DATABASE_URL',
|
||||
value:
|
||||
'postgres://postgres:[PASSWORD]@postgres-service:5432/[SUBDOMAIN]?sslmode=disable',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_USERNAME',
|
||||
value: '[USER]',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_PASSWORD',
|
||||
value: '[PASSWORD]',
|
||||
},
|
||||
],
|
||||
ports: [
|
||||
{
|
||||
port: 8080,
|
||||
type: 'http',
|
||||
publish: true,
|
||||
rateLimit: null,
|
||||
},
|
||||
],
|
||||
healthCheck: null,
|
||||
};
|
||||
|
||||
expect(formattedConfig).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import type {
|
||||
Port,
|
||||
ServiceFormInitialData,
|
||||
ServiceFormValues,
|
||||
} from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
|
||||
import type { ConfigRunServiceConfigInsertInput } from '@/utils/__generated__/graphql';
|
||||
import { removeTypename } from '@/utils/helpers';
|
||||
|
||||
export interface GetFormattedServiceConfigProps {
|
||||
values: ServiceFormValues;
|
||||
initialData?: ServiceFormInitialData;
|
||||
}
|
||||
|
||||
export default function getFormattedServiceConfig({
|
||||
values,
|
||||
initialData,
|
||||
}: GetFormattedServiceConfigProps) {
|
||||
// Remove any __typename property from the values
|
||||
const sanitizedValues = removeTypename(values) as ServiceFormValues;
|
||||
const sanitizedInitialDataPorts: Port[] = initialData?.ports
|
||||
? removeTypename(initialData.ports)
|
||||
: [];
|
||||
|
||||
const config: ConfigRunServiceConfigInsertInput = {
|
||||
name: sanitizedValues.name,
|
||||
image: {
|
||||
image: sanitizedValues.image,
|
||||
pullCredentials: sanitizedValues.pullCredentials,
|
||||
},
|
||||
command: sanitizedValues.command?.map((arg) => arg.argument),
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: sanitizedValues.compute?.cpu,
|
||||
memory: sanitizedValues.compute?.memory,
|
||||
},
|
||||
storage: sanitizedValues.storage?.map((item) => ({
|
||||
name: item.name,
|
||||
path: item.path,
|
||||
capacity: item.capacity,
|
||||
})),
|
||||
replicas: sanitizedValues.replicas,
|
||||
autoscaler: sanitizedValues.autoscaler
|
||||
? {
|
||||
maxReplicas: sanitizedValues.autoscaler?.maxReplicas,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
environment: sanitizedValues.environment?.map((item) => ({
|
||||
name: item.name,
|
||||
value: item.value,
|
||||
})),
|
||||
ports: sanitizedValues.ports?.map((item) => ({
|
||||
port: item.port,
|
||||
type: item.type,
|
||||
publish: item.publish,
|
||||
ingresses: item.ingresses as any, // cannot be changed on the UI always null type checking can be skipped.
|
||||
rateLimit:
|
||||
sanitizedInitialDataPorts.find(
|
||||
(port) => port.port === item.port && port.type === item.type,
|
||||
)?.rateLimit ?? (null as any), // cannot be changed on the UI always null type checking can be skipped.
|
||||
})),
|
||||
healthCheck: sanitizedValues.healthCheck
|
||||
? {
|
||||
port: sanitizedValues.healthCheck?.port,
|
||||
initialDelaySeconds: sanitizedValues.healthCheck?.initialDelaySeconds,
|
||||
probePeriodSeconds: sanitizedValues.healthCheck?.probePeriodSeconds,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default as getFormattedServiceConfig } from './getFormattedServiceConfig';
|
||||
@@ -0,0 +1 @@
|
||||
export { default as parseConfigFromInstallLink } from './parseConfigFromInstallLink';
|
||||
@@ -0,0 +1,156 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import parseConfigFromInstallLink from './parseConfigFromInstallLink';
|
||||
|
||||
describe('parseConfigFromInstallLink', () => {
|
||||
it('pghero config without autoscaler should be formatted correctly', () => {
|
||||
const pgheroBase64Config =
|
||||
'eyJuYW1lIjoicGdoZXJvIiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vYW5rYW5lL3BnaGVybzpsYXRlc3QifSwiY29tbWFuZCI6W10sInJlc291cmNlcyI6eyJjb21wdXRlIjp7ImNwdSI6MTI1LCJtZW1vcnkiOjI1Nn0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MX0sImVudmlyb25tZW50IjpbeyJuYW1lIjoiREFUQUJBU0VfVVJMIiwidmFsdWUiOiJwb3N0Z3JlczovL3Bvc3RncmVzOltQQVNTV09SRF1AcG9zdGdyZXMtc2VydmljZTo1NDMyL1tTVUJET01BSU5dP3NzbG1vZGU9ZGlzYWJsZSJ9LHsibmFtZSI6IlBHSEVST19VU0VSTkFNRSIsInZhbHVlIjoiW1VTRVJdIn0seyJuYW1lIjoiUEdIRVJPX1BBU1NXT1JEIiwidmFsdWUiOiJbUEFTU1dPUkRdIn1dLCJwb3J0cyI6W3sicG9ydCI6ODA4MCwidHlwZSI6Imh0dHAiLCJwdWJsaXNoIjp0cnVlfV19';
|
||||
|
||||
const config = parseConfigFromInstallLink(pgheroBase64Config);
|
||||
|
||||
const expected = {
|
||||
name: 'pghero',
|
||||
image: 'docker.io/ankane/pghero:latest',
|
||||
command: [],
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
storage: [],
|
||||
replicas: 1,
|
||||
},
|
||||
environment: [
|
||||
{
|
||||
name: 'DATABASE_URL',
|
||||
value:
|
||||
'postgres://postgres:[PASSWORD]@postgres-service:5432/[SUBDOMAIN]?sslmode=disable',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_USERNAME',
|
||||
value: '[USER]',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_PASSWORD',
|
||||
value: '[PASSWORD]',
|
||||
},
|
||||
],
|
||||
ports: [
|
||||
{
|
||||
port: 8080,
|
||||
type: 'http',
|
||||
publish: true,
|
||||
},
|
||||
],
|
||||
autoscaler: null,
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
replicas: 1,
|
||||
storage: [],
|
||||
};
|
||||
|
||||
expect(config).toEqual(expected);
|
||||
});
|
||||
|
||||
it('antivirus config without autoscaler should be formatted correctly', () => {
|
||||
const antivirusBase64Config =
|
||||
'eyJuYW1lIjoiY2xhbWF2IiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vbmhvc3QvY2xhbWF2OjAuMS4xIn0sImNvbW1hbmQiOltdLCJyZXNvdXJjZXMiOnsiY29tcHV0ZSI6eyJjcHUiOjEwMDAsIm1lbW9yeSI6MjA0OH0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MX0sImVudmlyb25tZW50IjpbXSwicG9ydHMiOlt7InBvcnQiOiIzMzEwIiwidHlwZSI6InRjcCIsInB1Ymxpc2giOmZhbHNlfV19';
|
||||
|
||||
const config = parseConfigFromInstallLink(antivirusBase64Config);
|
||||
|
||||
const expected = {
|
||||
name: 'clamav',
|
||||
image: 'docker.io/nhost/clamav:0.1.1',
|
||||
command: [],
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: 1000,
|
||||
memory: 2048,
|
||||
},
|
||||
storage: [],
|
||||
replicas: 1,
|
||||
},
|
||||
environment: [],
|
||||
ports: [
|
||||
{
|
||||
port: '3310',
|
||||
type: 'tcp',
|
||||
publish: false,
|
||||
},
|
||||
],
|
||||
autoscaler: null,
|
||||
compute: {
|
||||
cpu: 1000,
|
||||
memory: 2048,
|
||||
},
|
||||
replicas: 1,
|
||||
storage: [],
|
||||
};
|
||||
expect(config).toEqual(expected);
|
||||
});
|
||||
|
||||
it('invalid config should throw an error', () => {
|
||||
const invalidBase64Config = 'invalid';
|
||||
|
||||
expect(() => parseConfigFromInstallLink(invalidBase64Config)).toThrow();
|
||||
});
|
||||
|
||||
it('pghero config with autoscaler should be formatted correctly', () => {
|
||||
const pgheroWithAutoscalerBase64 =
|
||||
'eyJuYW1lIjoicGdoZXJvIiwiaW1hZ2UiOnsiaW1hZ2UiOiJkb2NrZXIuaW8vYW5rYW5lL3BnaGVybzpsYXRlc3QifSwiY29tbWFuZCI6W10sInJlc291cmNlcyI6eyJjb21wdXRlIjp7ImNwdSI6MTI1LCJtZW1vcnkiOjI1Nn0sInN0b3JhZ2UiOltdLCJyZXBsaWNhcyI6MSwiYXV0b3NjYWxlciI6eyJtYXhSZXBsaWNhcyI6MTF9fSwiZW52aXJvbm1lbnQiOlt7Im5hbWUiOiJEQVRBQkFTRV9VUkwiLCJ2YWx1ZSI6InBvc3RncmVzOi8vcG9zdGdyZXM6W1BBU1NXT1JEXUBwb3N0Z3Jlcy1zZXJ2aWNlOjU0MzIvW1NVQkRPTUFJTl0/c3NsbW9kZT1kaXNhYmxlIn0seyJuYW1lIjoiUEdIRVJPX1VTRVJOQU1FIiwidmFsdWUiOiJbVVNFUl0ifSx7Im5hbWUiOiJQR0hFUk9fUEFTU1dPUkQiLCJ2YWx1ZSI6IltQQVNTV09SRF0ifV0sInBvcnRzIjpbeyJwb3J0Ijo4MDgwLCJ0eXBlIjoiaHR0cCIsInB1Ymxpc2giOnRydWUsInJhdGVMaW1pdCI6bnVsbH1dLCJoZWFsdGhDaGVjayI6bnVsbH0=';
|
||||
|
||||
const config = parseConfigFromInstallLink(pgheroWithAutoscalerBase64);
|
||||
|
||||
const expected = {
|
||||
name: 'pghero',
|
||||
image: 'docker.io/ankane/pghero:latest',
|
||||
command: [],
|
||||
resources: {
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
storage: [],
|
||||
replicas: 1,
|
||||
autoscaler: {
|
||||
maxReplicas: 11,
|
||||
},
|
||||
},
|
||||
environment: [
|
||||
{
|
||||
name: 'DATABASE_URL',
|
||||
value:
|
||||
'postgres://postgres:[PASSWORD]@postgres-service:5432/[SUBDOMAIN]?sslmode=disable',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_USERNAME',
|
||||
value: '[USER]',
|
||||
},
|
||||
{
|
||||
name: 'PGHERO_PASSWORD',
|
||||
value: '[PASSWORD]',
|
||||
},
|
||||
],
|
||||
ports: [
|
||||
{
|
||||
port: 8080,
|
||||
type: 'http',
|
||||
publish: true,
|
||||
},
|
||||
],
|
||||
autoscaler: {
|
||||
maxReplicas: 11,
|
||||
},
|
||||
compute: {
|
||||
cpu: 125,
|
||||
memory: 256,
|
||||
},
|
||||
replicas: 1,
|
||||
storage: [],
|
||||
};
|
||||
|
||||
expect(config).toEqual(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { type RunServiceConfig } from '@/features/orgs/projects/common/hooks/useRunServices';
|
||||
import { type PortTypes } from '@/features/orgs/projects/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
|
||||
import {
|
||||
defaultServiceFormValues,
|
||||
type ServiceFormInitialData,
|
||||
} from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
|
||||
|
||||
export default function parseConfigFromInstallLink(
|
||||
base64Config: string,
|
||||
): ServiceFormInitialData {
|
||||
const decodedConfig = atob(base64Config);
|
||||
const parsedConfig: RunServiceConfig = JSON.parse(decodedConfig);
|
||||
const initialData = {
|
||||
...parsedConfig,
|
||||
autoscaler:
|
||||
parsedConfig?.resources?.autoscaler ??
|
||||
defaultServiceFormValues.autoscaler,
|
||||
compute:
|
||||
parsedConfig?.resources?.compute ?? defaultServiceFormValues.compute,
|
||||
image: parsedConfig?.image?.image,
|
||||
command: parsedConfig?.command?.map((arg) => ({
|
||||
argument: arg,
|
||||
})),
|
||||
environment:
|
||||
parsedConfig?.environment?.map((env) => ({
|
||||
name: env.name,
|
||||
value: env.value,
|
||||
})) ?? undefined,
|
||||
healthCheck: parsedConfig?.healthCheck
|
||||
? {
|
||||
port: parsedConfig.healthCheck.port ?? 3000,
|
||||
initialDelaySeconds:
|
||||
parsedConfig.healthCheck.initialDelaySeconds ?? 30,
|
||||
probePeriodSeconds: parsedConfig.healthCheck.probePeriodSeconds ?? 60,
|
||||
}
|
||||
: undefined,
|
||||
ports:
|
||||
parsedConfig?.ports?.map((item) => ({
|
||||
port: item.port ?? 3000,
|
||||
type: item.type as PortTypes,
|
||||
publish: Boolean(item.publish),
|
||||
})) ?? [],
|
||||
replicas: parsedConfig?.resources?.replicas,
|
||||
storage: parsedConfig?.resources?.storage ?? undefined,
|
||||
};
|
||||
|
||||
return initialData;
|
||||
}
|
||||
@@ -11,16 +11,12 @@ import { ServicesIcon } from '@/components/ui/v2/icons/ServicesIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { OrgLayout } from '@/features/orgs/layout/OrgLayout';
|
||||
import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatform';
|
||||
import {
|
||||
useRunServices,
|
||||
type RunServiceConfig,
|
||||
} from '@/features/orgs/projects/common/hooks/useRunServices';
|
||||
import { useRunServices } from '@/features/orgs/projects/common/hooks/useRunServices';
|
||||
import { useCurrentOrg } from '@/features/orgs/projects/hooks/useCurrentOrg';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { ServiceForm } from '@/features/orgs/projects/services/components/ServiceForm';
|
||||
import { type PortTypes } from '@/features/orgs/projects/services/components/ServiceForm/components/PortsFormSection/PortsFormSectionTypes';
|
||||
import type { ServiceFormInitialData } from '@/features/orgs/projects/services/components/ServiceForm/ServiceFormTypes';
|
||||
import { ServicesList } from '@/features/orgs/projects/services/components/ServicesList';
|
||||
import { parseConfigFromInstallLink } from '@/features/orgs/projects/services/utils/parseConfigFromInstallLink';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback, useEffect, type ReactElement } from 'react';
|
||||
|
||||
@@ -48,29 +44,7 @@ export default function RunPage() {
|
||||
(base64Config: string) => {
|
||||
if (router.query?.config) {
|
||||
try {
|
||||
const decodedConfig = atob(base64Config);
|
||||
const parsedConfig: RunServiceConfig = JSON.parse(decodedConfig);
|
||||
const initialData = {
|
||||
...parsedConfig,
|
||||
autoscaler: parsedConfig?.resources?.autoscaler ?? {
|
||||
maxReplicas: 0,
|
||||
},
|
||||
compute: parsedConfig?.resources?.compute ?? {
|
||||
cpu: 62,
|
||||
memory: 128,
|
||||
},
|
||||
image: parsedConfig?.image?.image,
|
||||
command: parsedConfig?.command?.map((arg) => ({
|
||||
argument: arg,
|
||||
})),
|
||||
ports: parsedConfig?.ports?.map((item) => ({
|
||||
port: item.port,
|
||||
type: item.type as PortTypes,
|
||||
publish: item.publish,
|
||||
})),
|
||||
replicas: parsedConfig?.resources?.replicas,
|
||||
storage: parsedConfig?.resources?.storage,
|
||||
} as ServiceFormInitialData;
|
||||
const initialData = parseConfigFromInstallLink(base64Config);
|
||||
|
||||
openDrawer({
|
||||
title: (
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1326,10 +1326,9 @@ After we complete the next tutorial on user authentication, you will be able to
|
||||
|
||||
1. **Server-Side Helpers**: Utilities for handling authentication in Next.js server components and middleware
|
||||
2. **Middleware Route Protection**: Next.js middleware runs before any page renders, automatically redirecting unauthenticated users from protected routes and refreshing tokens
|
||||
3. **AuthProvider**: Client-side provider that manages authentication state using Nhost's client with cookie-based storage for server/client synchronization
|
||||
4. **Protected Pages**: Server components can assume authentication since middleware handles protection, focusing purely on rendering authenticated content
|
||||
5. **Navigation**: Server-side navigation component that adapts its links based on authentication status
|
||||
6. **Automatic Redirects**: All route protection and redirects are handled at the middleware level for optimal performance and security
|
||||
3. **Protected Pages**: Server components can assume authentication since middleware handles protection, focusing purely on rendering authenticated content
|
||||
4. **Navigation**: Server-side navigation component that adapts its links based on authentication status
|
||||
5. **Automatic Redirects**: All route protection and redirects are handled at the middleware level for optimal performance and security
|
||||
|
||||
## Key Features Demonstrated
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"js-yaml@<=4.1.0": ">=4.1.1"
|
||||
"js-yaml@<=4.1.0": ">=4.1.1",
|
||||
"glob@>=10.3.7 <=11.0.3": ">=11.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ After authorizing the GitHub integration, you'll need to tell Nhost a couple of
|
||||
|
||||
### Base Directory
|
||||
|
||||
This is the folder in your repository where your Nhost folder lives. If your Nhost foder is in the root of your repository, you can leave this as `/`. If it is in a subfolder (like `/backend`), specify that path here.
|
||||
This is the folder in your repository where your Nhost folder lives. If your Nhost folder is in the root of your repository, you can leave this as `/`. If it is in a subfolder (like `/backend`), specify that path here.
|
||||
|
||||
### Deployment Branch
|
||||
|
||||
|
||||
71
docs/pnpm-lock.yaml
generated
71
docs/pnpm-lock.yaml
generated
@@ -6,6 +6,7 @@ settings:
|
||||
|
||||
overrides:
|
||||
js-yaml@<=4.1.0: '>=4.1.1'
|
||||
glob@>=10.3.7 <=11.0.3: '>=11.1.0'
|
||||
|
||||
packageExtensionsChecksum: sha256-4+NJJHoeDEOtWI2UxgTNLimXyrOojBs00S85/9Babm0=
|
||||
|
||||
@@ -319,6 +320,14 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@isaacs/balanced-match@4.0.1':
|
||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -423,10 +432,6 @@ packages:
|
||||
'@openapi-contrib/openapi-schema-to-json-schema@3.2.0':
|
||||
resolution: {integrity: sha512-Gj6C0JwCr8arj0sYuslWXUBSP/KnUlEGnPW4qxlXvAl543oaNQgMgIgkQUA6vs5BCCvwTEiL8m/wdWzfl4UvSw==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@puppeteer/browsers@2.3.0':
|
||||
resolution: {integrity: sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1717,8 +1722,9 @@ packages:
|
||||
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
glob@10.4.5:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
glob@12.0.0:
|
||||
resolution: {integrity: sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==}
|
||||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
globalthis@1.0.4:
|
||||
@@ -2096,8 +2102,9 @@ packages:
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
jackspeak@4.1.1:
|
||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
jiti@1.21.7:
|
||||
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
||||
@@ -2183,8 +2190,9 @@ packages:
|
||||
resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
lru-cache@11.2.2:
|
||||
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@7.18.3:
|
||||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||
@@ -2424,6 +2432,10 @@ packages:
|
||||
resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
minimatch@10.1.1:
|
||||
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
@@ -2637,9 +2649,9 @@ packages:
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
path-scurry@2.0.1:
|
||||
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
path-to-regexp@0.1.12:
|
||||
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
|
||||
@@ -3842,6 +3854,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 24.7.0
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
dependencies:
|
||||
'@isaacs/balanced-match': 4.0.1
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
@@ -4222,9 +4240,6 @@ snapshots:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@puppeteer/browsers@2.3.0':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@@ -5644,14 +5659,14 @@ snapshots:
|
||||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
|
||||
glob@10.4.5:
|
||||
glob@12.0.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
jackspeak: 4.1.1
|
||||
minimatch: 10.1.1
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
path-scurry: 2.0.1
|
||||
|
||||
globalthis@1.0.4:
|
||||
dependencies:
|
||||
@@ -6164,11 +6179,9 @@ snapshots:
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
jackspeak@4.1.1:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jiti@1.21.7: {}
|
||||
|
||||
@@ -6236,7 +6249,7 @@ snapshots:
|
||||
|
||||
lowercase-keys@3.0.0: {}
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
lru-cache@11.2.2: {}
|
||||
|
||||
lru-cache@7.18.3: {}
|
||||
|
||||
@@ -6755,6 +6768,10 @@ snapshots:
|
||||
|
||||
mimic-response@4.0.0: {}
|
||||
|
||||
minimatch@10.1.1:
|
||||
dependencies:
|
||||
'@isaacs/brace-expansion': 5.0.0
|
||||
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
@@ -6989,9 +7006,9 @@ snapshots:
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
path-scurry@2.0.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
lru-cache: 11.2.2
|
||||
minipass: 7.1.2
|
||||
|
||||
path-to-regexp@0.1.12: {}
|
||||
@@ -7744,7 +7761,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
commander: 4.1.1
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
lines-and-columns: 1.2.4
|
||||
mz: 2.7.0
|
||||
pirates: 4.0.7
|
||||
|
||||
9
docs/products/graphql/guides/codegen-nhost.mdx
Normal file
9
docs/products/graphql/guides/codegen-nhost.mdx
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
title: "Codegen + Nhost"
|
||||
description: "How to use The Guild's codegen with Nhost"
|
||||
icon: U
|
||||
---
|
||||
|
||||
You can use [The Guild's codegen](https://the-guild.dev/graphql/codegen) to generate types and document nodes for your GraphQL operations and use them directly with Nhost's GraphQL client.
|
||||
|
||||
You can find a working example with instructions in our [GitHub repository](https://github.com/nhost/nhost/blob/main/examples/guides/codegen-nhost/README.md).
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"private": true,
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"js-yaml@<=4.1.0": ">=4.1.1"
|
||||
"js-yaml@<=4.1.0": ">=4.1.1",
|
||||
"glob@>=10.3.7 <=11.0.3": ">=11.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
84
examples/demos/ReactNativeDemo/pnpm-lock.yaml
generated
84
examples/demos/ReactNativeDemo/pnpm-lock.yaml
generated
@@ -6,6 +6,7 @@ settings:
|
||||
|
||||
overrides:
|
||||
js-yaml@<=4.1.0: '>=4.1.1'
|
||||
glob@>=10.3.7 <=11.0.3: '>=11.1.0'
|
||||
|
||||
importers:
|
||||
|
||||
@@ -716,6 +717,14 @@ packages:
|
||||
resolution: {integrity: sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw==}
|
||||
hasBin: true
|
||||
|
||||
'@isaacs/balanced-match@4.0.1':
|
||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -776,10 +785,6 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.30':
|
||||
resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2':
|
||||
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
|
||||
peerDependencies:
|
||||
@@ -1698,8 +1703,9 @@ packages:
|
||||
resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
glob@10.4.5:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
glob@12.0.0:
|
||||
resolution: {integrity: sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==}
|
||||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
glob@7.2.3:
|
||||
@@ -1827,8 +1833,9 @@ packages:
|
||||
resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
jackspeak@4.1.1:
|
||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
jest-environment-node@29.7.0:
|
||||
resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==}
|
||||
@@ -2007,6 +2014,10 @@ packages:
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@11.2.2:
|
||||
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
@@ -2109,6 +2120,10 @@ packages:
|
||||
resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
minimatch@10.1.1:
|
||||
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
@@ -2272,9 +2287,9 @@ packages:
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
path-scurry@2.0.1:
|
||||
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
@@ -3588,7 +3603,7 @@ snapshots:
|
||||
env-editor: 0.4.2
|
||||
freeport-async: 2.0.0
|
||||
getenv: 2.0.0
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
lan-network: 0.1.7
|
||||
minimatch: 9.0.5
|
||||
node-forge: 1.3.1
|
||||
@@ -3636,7 +3651,7 @@ snapshots:
|
||||
chalk: 4.1.2
|
||||
debug: 4.4.1
|
||||
getenv: 2.0.0
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.7.2
|
||||
slash: 3.0.0
|
||||
@@ -3656,7 +3671,7 @@ snapshots:
|
||||
'@expo/json-file': 9.1.5
|
||||
deepmerge: 4.3.1
|
||||
getenv: 2.0.0
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
require-from-string: 2.0.2
|
||||
resolve-from: 5.0.0
|
||||
resolve-workspace-root: 2.0.0
|
||||
@@ -3670,7 +3685,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@expo/sudo-prompt': 9.3.2
|
||||
debug: 3.2.7
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -3692,7 +3707,7 @@ snapshots:
|
||||
debug: 4.4.1
|
||||
find-up: 5.0.0
|
||||
getenv: 2.0.0
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
ignore: 5.3.2
|
||||
minimatch: 9.0.5
|
||||
p-limit: 3.1.0
|
||||
@@ -3733,7 +3748,7 @@ snapshots:
|
||||
dotenv: 16.4.7
|
||||
dotenv-expand: 11.0.7
|
||||
getenv: 2.0.0
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
jsc-safe-url: 0.2.4
|
||||
lightningcss: 1.27.0
|
||||
minimatch: 9.0.5
|
||||
@@ -3815,6 +3830,12 @@ snapshots:
|
||||
find-up: 5.0.0
|
||||
js-yaml: 4.1.1
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
dependencies:
|
||||
'@isaacs/balanced-match': 4.0.1
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
@@ -3912,9 +3933,6 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.0.14)(react@19.0.0)':
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
@@ -4775,7 +4793,7 @@ snapshots:
|
||||
chalk: 4.1.2
|
||||
commander: 7.2.0
|
||||
find-up: 5.0.0
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
require-from-string: 2.0.2
|
||||
resolve-from: 5.0.0
|
||||
|
||||
@@ -4951,14 +4969,14 @@ snapshots:
|
||||
|
||||
getenv@2.0.0: {}
|
||||
|
||||
glob@10.4.5:
|
||||
glob@12.0.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
jackspeak: 4.1.1
|
||||
minimatch: 10.1.1
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
path-scurry: 2.0.1
|
||||
|
||||
glob@7.2.3:
|
||||
dependencies:
|
||||
@@ -5078,11 +5096,9 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
jackspeak@3.4.3:
|
||||
jackspeak@4.1.1:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jest-environment-node@29.7.0:
|
||||
dependencies:
|
||||
@@ -5258,6 +5274,8 @@ snapshots:
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.2: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
@@ -5468,6 +5486,10 @@ snapshots:
|
||||
|
||||
mimic-fn@1.2.0: {}
|
||||
|
||||
minimatch@10.1.1:
|
||||
dependencies:
|
||||
'@isaacs/brace-expansion': 5.0.0
|
||||
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
@@ -5606,9 +5628,9 @@ snapshots:
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
path-scurry@2.0.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
lru-cache: 11.2.2
|
||||
minipass: 7.1.2
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
@@ -6038,7 +6060,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
commander: 4.1.1
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
lines-and-columns: 1.2.4
|
||||
mz: 2.7.0
|
||||
pirates: 4.0.7
|
||||
|
||||
25
examples/guides/codegen-nhost/.gitignore
vendored
Normal file
25
examples/guides/codegen-nhost/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.vite
|
||||
443
examples/guides/codegen-nhost/README.md
Normal file
443
examples/guides/codegen-nhost/README.md
Normal file
@@ -0,0 +1,443 @@
|
||||
# GraphQL Code Generation with Nhost SDK
|
||||
|
||||
This guide demonstrates how to use GraphQL Code Generator with TypedDocumentNode to get full type safety when working with the Nhost SDK.
|
||||
|
||||
Note: While the project uses React to illustrate usage, the generated types and documents can be used in any JavaScript/TypeScript environment.
|
||||
|
||||
## Overview
|
||||
|
||||
The Nhost SDK's GraphQL client supports `TypedDocumentNode` from `@graphql-typed-document-node/core`, allowing you to use generated types and documents for type-safe GraphQL operations. This guide shows you how to set up GraphQL Code Generator to work seamlessly with Nhost.
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install Dependencies
|
||||
|
||||
```bash
|
||||
npm install @nhost/nhost-js graphql graphql-typed-document-node/core
|
||||
# or
|
||||
yarn add @nhost/nhost-js graphql graphql-typed-document-node/core
|
||||
# or
|
||||
pnpm add @nhost/nhost-js graphql graphql-typed-document-node/core
|
||||
```
|
||||
|
||||
### 2. Install GraphQL CodeGen
|
||||
|
||||
Install the necessary code generation packages:
|
||||
|
||||
```bash
|
||||
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset @graphql-codegen/schema-ast
|
||||
# or
|
||||
pnpm add -D @graphql-codegen/cli @graphql-codegen/client-preset @graphql-codegen/schema-ast
|
||||
```
|
||||
|
||||
### 3. Configure GraphQL CodeGen
|
||||
|
||||
Create a `codegen.ts` file with the client preset configuration:
|
||||
|
||||
```typescript
|
||||
import type { CodegenConfig } from "@graphql-codegen/cli";
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema: [
|
||||
{
|
||||
"https://local.graphql.local.nhost.run/v1": {
|
||||
headers: {
|
||||
"x-hasura-admin-secret": "nhost-admin-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
documents: ["src/lib/graphql/**/*.graphql"],
|
||||
ignoreNoDocuments: true,
|
||||
generates: {
|
||||
"./src/lib/graphql/__generated__/": {
|
||||
preset: "client",
|
||||
presetConfig: {
|
||||
persistedDocuments: false,
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
"./add-query-source-plugin.cjs": {},
|
||||
},
|
||||
],
|
||||
config: {
|
||||
scalars: {
|
||||
UUID: "string",
|
||||
uuid: "string",
|
||||
timestamptz: "string",
|
||||
jsonb: "Record<string, any>",
|
||||
bigint: "number",
|
||||
bytea: "Buffer",
|
||||
citext: "string",
|
||||
},
|
||||
useTypeImports: true,
|
||||
},
|
||||
},
|
||||
"./schema.graphql": {
|
||||
plugins: ["schema-ast"],
|
||||
config: {
|
||||
includeDirectives: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### 4. Create the Custom Plugin
|
||||
|
||||
The Nhost SDK expects documents to have a `loc.source.body` property containing the query string. Create a custom plugin to add this:
|
||||
|
||||
**add-query-source-plugin.cjs:**
|
||||
|
||||
```javascript
|
||||
// Custom GraphQL Codegen plugin to add loc.source.body to generated documents
|
||||
// This allows the Nhost SDK to extract the query string without needing the graphql package
|
||||
|
||||
const { print } = require("graphql");
|
||||
|
||||
/**
|
||||
* @type {import('@graphql-codegen/plugin-helpers').PluginFunction}
|
||||
*/
|
||||
const plugin = (_schema, documents, _config) => {
|
||||
let output = "";
|
||||
|
||||
for (const doc of documents) {
|
||||
if (!doc.document) continue;
|
||||
|
||||
for (const definition of doc.document.definitions) {
|
||||
if (definition.kind === "OperationDefinition" && definition.name) {
|
||||
const operationName = definition.name.value;
|
||||
const documentName = `${operationName}Document`;
|
||||
|
||||
// Create a document with just this operation
|
||||
const singleOpDocument = {
|
||||
kind: "Document",
|
||||
definitions: [definition],
|
||||
};
|
||||
|
||||
// Use graphql print to convert AST to string
|
||||
const source = print(singleOpDocument);
|
||||
|
||||
output += `
|
||||
// Add query source to ${documentName}
|
||||
if (${documentName}) {
|
||||
Object.assign(${documentName}, {
|
||||
loc: { source: { body: ${JSON.stringify(source)} } }
|
||||
});
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
module.exports = { plugin };
|
||||
```
|
||||
|
||||
## Integration Guide
|
||||
|
||||
### 1. Create an Auth Provider
|
||||
|
||||
Create an authentication context to manage the Nhost client and user session:
|
||||
|
||||
```typescript
|
||||
// src/lib/nhost/AuthProvider.tsx
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
useMemo,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { createClient, type NhostClient } from "@nhost/nhost-js";
|
||||
import { type Session } from "@nhost/nhost-js/auth";
|
||||
|
||||
interface AuthContextType {
|
||||
user: Session["user"] | null;
|
||||
session: Session | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
nhost: NhostClient;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [user, setUser] = useState<Session["user"] | null>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
|
||||
// Create the nhost client
|
||||
const nhost = useMemo(
|
||||
() =>
|
||||
createClient({
|
||||
region: import.meta.env.VITE_NHOST_REGION || "local",
|
||||
subdomain: import.meta.env.VITE_NHOST_SUBDOMAIN || "local",
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
const currentSession = nhost.getUserSession();
|
||||
setUser(currentSession?.user || null);
|
||||
setSession(currentSession);
|
||||
setIsAuthenticated(!!currentSession);
|
||||
setIsLoading(false);
|
||||
|
||||
const unsubscribe = nhost.sessionStorage.onChange((currentSession) => {
|
||||
setUser(currentSession?.user || null);
|
||||
setSession(currentSession);
|
||||
setIsAuthenticated(!!currentSession);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [nhost]);
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
session,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
nhost,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
```
|
||||
|
||||
### 2. Set Up Your App Providers
|
||||
|
||||
Wrap your application with the Auth provider:
|
||||
|
||||
```tsx
|
||||
// src/main.tsx
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { AuthProvider } from "./lib/nhost/AuthProvider";
|
||||
|
||||
const Root = () => (
|
||||
<React.StrictMode>
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
if (!rootElement) throw new Error("Root element not found");
|
||||
|
||||
createRoot(rootElement).render(<Root />);
|
||||
```
|
||||
|
||||
### 3. Define GraphQL Operations
|
||||
|
||||
Create GraphQL files with your queries and mutations:
|
||||
|
||||
```graphql
|
||||
# src/lib/graphql/operations.graphql
|
||||
query GetNinjaTurtlesWithComments {
|
||||
ninjaTurtles {
|
||||
id
|
||||
name
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
comments {
|
||||
id
|
||||
comment
|
||||
createdAt
|
||||
user {
|
||||
id
|
||||
displayName
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {
|
||||
insertComment(object: { ninjaTurtleId: $ninjaTurtleId, comment: $comment }) {
|
||||
id
|
||||
comment
|
||||
createdAt
|
||||
ninjaTurtleId
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Generate TypeScript Types
|
||||
|
||||
Run the code generator:
|
||||
|
||||
```bash
|
||||
npx graphql-codegen
|
||||
```
|
||||
|
||||
You can also add a script to your `package.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"generate": "graphql-codegen --config codegen.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
npm run generate
|
||||
# or
|
||||
pnpm generate
|
||||
```
|
||||
|
||||
### 5. Use in Components
|
||||
|
||||
Use the generated types and documents with the Nhost SDK:
|
||||
|
||||
```tsx
|
||||
// src/pages/Home.tsx
|
||||
import { type JSX, useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
AddCommentDocument,
|
||||
GetNinjaTurtlesWithCommentsDocument,
|
||||
type GetNinjaTurtlesWithCommentsQuery,
|
||||
} from "../lib/graphql/__generated__/graphql";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
const { isLoading, nhost } = useAuth();
|
||||
const [activeCommentId, setActiveCommentId] = useState<string | null>(null);
|
||||
const [commentText, setCommentText] = useState("");
|
||||
const [activeTabId, setActiveTabId] = useState<string | null>(null);
|
||||
|
||||
const [data, setData] = useState<GetNinjaTurtlesWithCommentsQuery | null>(
|
||||
null,
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
// Fetch ninja turtles data
|
||||
const fetchNinjaTurtles = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await nhost.graphql.request(
|
||||
GetNinjaTurtlesWithCommentsDocument,
|
||||
{},
|
||||
);
|
||||
|
||||
if (result.body.errors) {
|
||||
throw new Error(result.body.errors[0]?.message);
|
||||
}
|
||||
|
||||
setData(result.body.data ?? null);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [nhost.graphql]);
|
||||
|
||||
// Load data on mount
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
fetchNinjaTurtles();
|
||||
}
|
||||
}, [isLoading, fetchNinjaTurtles]);
|
||||
|
||||
const addComment = async (ninjaTurtleId: string, comment: string) => {
|
||||
try {
|
||||
const result = await nhost.graphql.request(AddCommentDocument, {
|
||||
ninjaTurtleId,
|
||||
comment,
|
||||
});
|
||||
|
||||
if (result.body.errors) {
|
||||
throw new Error(result.body.errors[0]?.message);
|
||||
}
|
||||
|
||||
// Clear form and refetch data
|
||||
setCommentText("");
|
||||
setActiveCommentId(null);
|
||||
await fetchNinjaTurtles();
|
||||
} catch (err) {
|
||||
console.error("Error adding comment:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// ... rest of component
|
||||
}
|
||||
```
|
||||
|
||||
## Key Points
|
||||
|
||||
### Type-Safe GraphQL Requests
|
||||
|
||||
The Nhost SDK's `graphql.request()` method has overloads that support `TypedDocumentNode`:
|
||||
|
||||
```typescript
|
||||
// Type inference works automatically
|
||||
const result = await nhost.graphql.request(
|
||||
GetNinjaTurtlesWithCommentsDocument,
|
||||
{}, // Variables are type-checked
|
||||
);
|
||||
|
||||
// result.body.data is typed as GetNinjaTurtlesWithCommentsQuery | undefined
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **GraphQL Code Generator** creates `TypedDocumentNode` types and documents using the client preset
|
||||
2. **Custom Plugin** adds the `loc.source.body` property to each document at runtime
|
||||
3. **Nhost SDK** detects the `TypedDocumentNode`, extracts the query string from `loc.source.body`, and executes the request
|
||||
4. **TypeScript** infers response types automatically based on the document types
|
||||
|
||||
### Benefits
|
||||
|
||||
- ✅ Full type safety for queries, mutations, and variables
|
||||
- ✅ Automatic type inference - no manual type annotations needed
|
||||
- ✅ Type-checked variables prevent runtime errors
|
||||
- ✅ IntelliSense support in your IDE
|
||||
- ✅ Compile-time errors for invalid queries or mismatched types
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "not a valid graphql query" Error
|
||||
|
||||
If you see this error, make sure:
|
||||
1. The custom plugin (`add-query-source-plugin.cjs`) is in place
|
||||
2. The plugin is configured in your `codegen.ts`
|
||||
3. You've run `pnpm generate` after adding the plugin
|
||||
|
||||
### TypeScript Errors
|
||||
|
||||
If you get type errors:
|
||||
1. Make sure you're not passing explicit generic type parameters to `nhost.graphql.request()`
|
||||
2. Let TypeScript infer types from the document
|
||||
3. Pass an empty object `{}` for queries without variables
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [GraphQL Code Generator Docs](https://the-guild.dev/graphql/codegen)
|
||||
- [Nhost Documentation](https://docs.nhost.io)
|
||||
- [TypedDocumentNode](https://github.com/dotansimha/graphql-typed-document-node)
|
||||
44
examples/guides/codegen-nhost/add-query-source-plugin.cjs
Normal file
44
examples/guides/codegen-nhost/add-query-source-plugin.cjs
Normal file
@@ -0,0 +1,44 @@
|
||||
// Custom GraphQL Codegen plugin to add loc.source.body to generated documents
|
||||
// This allows the Nhost SDK to extract the query string without needing the graphql package
|
||||
|
||||
const { print } = require("graphql");
|
||||
|
||||
/**
|
||||
* @type {import('@graphql-codegen/plugin-helpers').PluginFunction}
|
||||
*/
|
||||
const plugin = (_schema, documents, _config) => {
|
||||
let output = "";
|
||||
|
||||
for (const doc of documents) {
|
||||
if (!doc.document) continue;
|
||||
|
||||
for (const definition of doc.document.definitions) {
|
||||
if (definition.kind === "OperationDefinition" && definition.name) {
|
||||
const operationName = definition.name.value;
|
||||
const documentName = `${operationName}Document`;
|
||||
|
||||
// Create a document with just this operation
|
||||
const singleOpDocument = {
|
||||
kind: "Document",
|
||||
definitions: [definition],
|
||||
};
|
||||
|
||||
// Use graphql print to convert AST to string
|
||||
const source = print(singleOpDocument);
|
||||
|
||||
output += `
|
||||
// Add query source to ${documentName}
|
||||
if (${documentName}) {
|
||||
Object.assign(${documentName}, {
|
||||
loc: { source: { body: ${JSON.stringify(source)} } }
|
||||
});
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
module.exports = { plugin };
|
||||
7
examples/guides/codegen-nhost/biome.json
Normal file
7
examples/guides/codegen-nhost/biome.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"root": false,
|
||||
"extends": "//",
|
||||
"linter": {
|
||||
"includes": ["**", "!src/lib/graphql/__generated__/*.ts"]
|
||||
}
|
||||
}
|
||||
27
examples/guides/codegen-nhost/codegen-wrapper.sh
Executable file
27
examples/guides/codegen-nhost/codegen-wrapper.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Running GraphQL code generator..."
|
||||
pnpm graphql-codegen --config codegen.ts
|
||||
|
||||
GENERATED_TS_FILE="src/lib/graphql/__generated__/"
|
||||
GENERATED_SCHEMA_FILE="schema.graphql"
|
||||
|
||||
if [ -d "$GENERATED_TS_FILE" ]; then
|
||||
echo "Formatting $GENERATED_TS_FILE..."
|
||||
biome check --write "$GENERATED_TS_FILE"
|
||||
else
|
||||
echo "Error: Generated TypeScript file not found at $GENERATED_TS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
if [ -f "$GENERATED_SCHEMA_FILE" ]; then
|
||||
echo "Formatting $GENERATED_SCHEMA_FILE..."
|
||||
biome check --write "$GENERATED_SCHEMA_FILE"
|
||||
echo "Successfully formatted $GENERATED_SCHEMA_FILE"
|
||||
else
|
||||
echo "Warning: Generated schema file not found at $GENERATED_SCHEMA_FILE"
|
||||
fi
|
||||
|
||||
echo "All tasks completed successfully."
|
||||
48
examples/guides/codegen-nhost/codegen.ts
Normal file
48
examples/guides/codegen-nhost/codegen.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { CodegenConfig } from "@graphql-codegen/cli";
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema: [
|
||||
{
|
||||
"https://local.graphql.local.nhost.run/v1": {
|
||||
headers: {
|
||||
"x-hasura-admin-secret": "nhost-admin-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
documents: ["src/lib/graphql/**/*.graphql"],
|
||||
ignoreNoDocuments: true,
|
||||
generates: {
|
||||
"./src/lib/graphql/__generated__/": {
|
||||
preset: "client",
|
||||
presetConfig: {
|
||||
persistedDocuments: false,
|
||||
},
|
||||
plugins: [
|
||||
{
|
||||
"./add-query-source-plugin.cjs": {},
|
||||
},
|
||||
],
|
||||
config: {
|
||||
scalars: {
|
||||
UUID: "string",
|
||||
uuid: "string",
|
||||
timestamptz: "string",
|
||||
jsonb: "Record<string, any>",
|
||||
bigint: "number",
|
||||
bytea: "Buffer",
|
||||
citext: "string",
|
||||
},
|
||||
useTypeImports: true,
|
||||
},
|
||||
},
|
||||
"./schema.graphql": {
|
||||
plugins: ["schema-ast"],
|
||||
config: {
|
||||
includeDirectives: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
13
examples/guides/codegen-nhost/index.html
Normal file
13
examples/guides/codegen-nhost/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
40
examples/guides/codegen-nhost/package.json
Normal file
40
examples/guides/codegen-nhost/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "guides/codegen-nhost",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"generate": "bash codegen-wrapper.sh",
|
||||
"test": "pnpm test:typecheck && pnpm test:lint",
|
||||
"test:typecheck": "tsc --noEmit",
|
||||
"test:lint": "biome check",
|
||||
"format": "biome format --write",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@graphql-typed-document-node/core": "^3.2.0",
|
||||
"@nhost/nhost-js": "workspace:*",
|
||||
"graphql": "^16.11.0",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-router-dom": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^5.0.6",
|
||||
"@graphql-codegen/client-preset": "^5.1.2",
|
||||
"@graphql-codegen/schema-ast": "^4.1.0",
|
||||
"@graphql-codegen/typescript": "^4.1.6",
|
||||
"@graphql-codegen/typescript-operations": "^4.6.1",
|
||||
"@types/node": "^22.15.17",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.4.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"js-yaml@<4.1.1": ">=4.1.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
3826
examples/guides/codegen-nhost/pnpm-lock.yaml
generated
Normal file
3826
examples/guides/codegen-nhost/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
examples/guides/codegen-nhost/public/vite.svg
Normal file
1
examples/guides/codegen-nhost/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
10143
examples/guides/codegen-nhost/schema.graphql
Normal file
10143
examples/guides/codegen-nhost/schema.graphql
Normal file
File diff suppressed because it is too large
Load Diff
56
examples/guides/codegen-nhost/src/App.tsx
Normal file
56
examples/guides/codegen-nhost/src/App.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { JSX } from "react";
|
||||
import {
|
||||
createBrowserRouter,
|
||||
createRoutesFromElements,
|
||||
Navigate,
|
||||
Outlet,
|
||||
Route,
|
||||
RouterProvider,
|
||||
} from "react-router-dom";
|
||||
import Navigation from "./components/Navigation";
|
||||
import ProtectedRoute from "./components/ProtectedRoute";
|
||||
import Home from "./pages/Home";
|
||||
import Profile from "./pages/Profile";
|
||||
import SignIn from "./pages/SignIn";
|
||||
import SignUp from "./pages/SignUp";
|
||||
|
||||
// Root layout component to wrap all routes
|
||||
const RootLayout = (): JSX.Element => {
|
||||
return (
|
||||
<div className="flex-col min-h-screen">
|
||||
<Navigation />
|
||||
<main className="max-w-2xl mx-auto p-6 w-full">
|
||||
<Outlet />
|
||||
</main>
|
||||
<footer>
|
||||
<p
|
||||
className="text-sm text-center"
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
>
|
||||
© {new Date().getFullYear()} Nhost Demo
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Create router with routes
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route element={<RootLayout />}>
|
||||
<Route path="signin" element={<SignIn />} />
|
||||
<Route path="signup" element={<SignUp />} />
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path="home" element={<Home />} />
|
||||
<Route path="profile" element={<Profile />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to="/" />} />
|
||||
</Route>,
|
||||
),
|
||||
);
|
||||
|
||||
const App = (): JSX.Element => {
|
||||
return <RouterProvider router={router} />;
|
||||
};
|
||||
|
||||
export default App;
|
||||
85
examples/guides/codegen-nhost/src/components/Navigation.tsx
Normal file
85
examples/guides/codegen-nhost/src/components/Navigation.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { JSX } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function Navigation(): JSX.Element {
|
||||
const { isAuthenticated, nhost, session } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
// Helper function to determine if a link is active
|
||||
const isActive = (path: string): string => {
|
||||
return location.pathname === path ? "active" : "";
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="navbar">
|
||||
<div className="navbar-container">
|
||||
<div className="flex items-center">
|
||||
<span className="navbar-brand">Nhost Demo</span>
|
||||
<div className="navbar-links">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<Link to="/home" className={`nav-link ${isActive("/home")}`}>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
to="/profile"
|
||||
className={`nav-link ${isActive("/profile")}`}
|
||||
>
|
||||
Profile
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
to="/signin"
|
||||
className={`nav-link ${isActive("/signin")}`}
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
<Link
|
||||
to="/signup"
|
||||
className={`nav-link ${isActive("/signup")}`}
|
||||
>
|
||||
Sign Up
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAuthenticated && (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
if (session) {
|
||||
await nhost.auth.signOut({
|
||||
refreshToken: session.refreshToken,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="icon-button"
|
||||
title="Sign Out"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-label="Sign Out"
|
||||
role="img"
|
||||
>
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
redirectTo?: string;
|
||||
}
|
||||
|
||||
export default function ProtectedRoute({
|
||||
redirectTo = "/signin",
|
||||
}: ProtectedRouteProps) {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to={redirectTo} />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
}
|
||||
552
examples/guides/codegen-nhost/src/index.css
Normal file
552
examples/guides/codegen-nhost/src/index.css
Normal file
@@ -0,0 +1,552 @@
|
||||
/* Base styles */
|
||||
:root {
|
||||
--background: #030712;
|
||||
--foreground: #ffffff;
|
||||
--card-bg: #111827;
|
||||
--card-border: #1f2937;
|
||||
--primary: #6366f1;
|
||||
--primary-hover: #4f46e5;
|
||||
--secondary: #10b981;
|
||||
--secondary-hover: #059669;
|
||||
--accent: #8b5cf6;
|
||||
--accent-hover: #7c3aed;
|
||||
--success: #22c55e;
|
||||
--error: #ef4444;
|
||||
--text-primary: #f9fafb;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-muted: #9ca3af;
|
||||
--border-color: rgba(31, 41, 55, 0.7);
|
||||
--font-geist-mono:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.min-h-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.max-w-2xl {
|
||||
max-width: 42rem;
|
||||
}
|
||||
|
||||
.mx-auto {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.p-6 {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.p-8 {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.py-5 {
|
||||
padding-top: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mt-4 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.mr-8 {
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.space-y-5 > * + * {
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.space-x-4 > * + * {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-weight: bold;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.text-3xl {
|
||||
font-size: 1.875rem;
|
||||
}
|
||||
|
||||
.text-2xl {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.font-bold {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(to right, var(--primary), var(--accent));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
/* Components */
|
||||
.glass-card {
|
||||
background: rgba(17, 24, 39, 0.7);
|
||||
backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 0.625rem 1rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: var(--primary-hover);
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--secondary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background-color: var(--secondary-hover);
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: white;
|
||||
background-color: rgba(31, 41, 55, 0.7);
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
border: 1px solid var(--border-color);
|
||||
color: white;
|
||||
border-radius: 0.375rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: rgba(239, 68, 68, 0.2);
|
||||
border: 1px solid rgba(239, 68, 68, 0.5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: rgba(34, 197, 94, 0.2);
|
||||
border: 1px solid rgba(34, 197, 94, 0.5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.navbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: rgba(17, 24, 39, 0.8);
|
||||
backdrop-filter: blur(8px);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 1rem 0;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.navbar-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 42rem;
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
color: var(--primary);
|
||||
font-weight: bold;
|
||||
font-size: 1.125rem;
|
||||
margin-right: 2rem;
|
||||
}
|
||||
|
||||
.navbar-links {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: rgba(31, 41, 55, 0.3);
|
||||
}
|
||||
|
||||
/* File upload styles */
|
||||
.file-upload {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.5rem;
|
||||
border: 2px dashed rgba(99, 102, 241, 0.3);
|
||||
border-radius: 0.5rem;
|
||||
background-color: rgba(31, 41, 55, 0.3);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-upload:hover {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
padding: 1.25rem 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Link styles */
|
||||
a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Loading state */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 50vh;
|
||||
}
|
||||
|
||||
/* Code blocks */
|
||||
pre {
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
overflow: auto;
|
||||
font-family: monospace;
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Profile data */
|
||||
.profile-item {
|
||||
padding-bottom: 0.75rem;
|
||||
margin-bottom: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.profile-item strong {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.action-link {
|
||||
color: var(--primary);
|
||||
font-weight: 500;
|
||||
margin-right: 0.75rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-link:hover {
|
||||
color: var(--primary-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.action-link-danger {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.action-link-danger:hover {
|
||||
color: #f05252;
|
||||
}
|
||||
|
||||
/* Icon button */
|
||||
.icon-button {
|
||||
background-color: transparent;
|
||||
color: var(--primary);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.icon-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.icon-button svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Table action icons */
|
||||
.table-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.action-icon svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.action-icon:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.action-icon-view {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.action-icon-view:hover:not(:disabled) {
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
.action-icon-delete {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.action-icon-delete:hover:not(:disabled) {
|
||||
background-color: rgba(239, 68, 68, 0.1);
|
||||
color: #f05252;
|
||||
}
|
||||
|
||||
/* Tab styles */
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
flex: 1;
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
background-color: rgba(31, 41, 55, 0.5);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.tab-button:hover:not(.tab-active) {
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.tab-button.tab-active {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.tab-button:first-child {
|
||||
border-top-left-radius: 0.5rem;
|
||||
border-bottom-left-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-button:last-child {
|
||||
border-top-right-radius: 0.5rem;
|
||||
border-bottom-right-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
110
examples/guides/codegen-nhost/src/lib/graphql/__generated__/fragment-masking.ts
generated
Normal file
110
examples/guides/codegen-nhost/src/lib/graphql/__generated__/fragment-masking.ts
generated
Normal file
@@ -0,0 +1,110 @@
|
||||
/* eslint-disable */
|
||||
import type {
|
||||
DocumentTypeDecoration,
|
||||
ResultOf,
|
||||
TypedDocumentNode,
|
||||
} from "@graphql-typed-document-node/core";
|
||||
import type { FragmentDefinitionNode } from "graphql";
|
||||
import type { Incremental } from "./graphql";
|
||||
|
||||
export type FragmentType<
|
||||
TDocumentType extends DocumentTypeDecoration<any, any>,
|
||||
> = TDocumentType extends DocumentTypeDecoration<infer TType, any>
|
||||
? [TType] extends [{ " $fragmentName"?: infer TKey }]
|
||||
? TKey extends string
|
||||
? { " $fragmentRefs"?: { [key in TKey]: TType } }
|
||||
: never
|
||||
: never
|
||||
: never;
|
||||
|
||||
// return non-nullable if `fragmentType` is non-nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>,
|
||||
): TType;
|
||||
// return nullable if `fragmentType` is undefined
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | undefined,
|
||||
): TType | undefined;
|
||||
// return nullable if `fragmentType` is nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null,
|
||||
): TType | null;
|
||||
// return nullable if `fragmentType` is nullable or undefined
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType:
|
||||
| FragmentType<DocumentTypeDecoration<TType, any>>
|
||||
| null
|
||||
| undefined,
|
||||
): TType | null | undefined;
|
||||
// return array of non-nullable if `fragmentType` is array of non-nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: Array<FragmentType<DocumentTypeDecoration<TType, any>>>,
|
||||
): Array<TType>;
|
||||
// return array of nullable if `fragmentType` is array of nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType:
|
||||
| Array<FragmentType<DocumentTypeDecoration<TType, any>>>
|
||||
| null
|
||||
| undefined,
|
||||
): Array<TType> | null | undefined;
|
||||
// return readonly array of non-nullable if `fragmentType` is array of non-nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>,
|
||||
): ReadonlyArray<TType>;
|
||||
// return readonly array of nullable if `fragmentType` is array of nullable
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType:
|
||||
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
|
||||
| null
|
||||
| undefined,
|
||||
): ReadonlyArray<TType> | null | undefined;
|
||||
export function useFragment<TType>(
|
||||
_documentNode: DocumentTypeDecoration<TType, any>,
|
||||
fragmentType:
|
||||
| FragmentType<DocumentTypeDecoration<TType, any>>
|
||||
| Array<FragmentType<DocumentTypeDecoration<TType, any>>>
|
||||
| ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
|
||||
| null
|
||||
| undefined,
|
||||
): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {
|
||||
return fragmentType as any;
|
||||
}
|
||||
|
||||
export function makeFragmentData<
|
||||
F extends DocumentTypeDecoration<any, any>,
|
||||
FT extends ResultOf<F>,
|
||||
>(data: FT, _fragment: F): FragmentType<F> {
|
||||
return data as FragmentType<F>;
|
||||
}
|
||||
export function isFragmentReady<TQuery, TFrag>(
|
||||
queryNode: DocumentTypeDecoration<TQuery, any>,
|
||||
fragmentNode: TypedDocumentNode<TFrag>,
|
||||
data:
|
||||
| FragmentType<TypedDocumentNode<Incremental<TFrag>, any>>
|
||||
| null
|
||||
| undefined,
|
||||
): data is FragmentType<typeof fragmentNode> {
|
||||
const deferredFields = (
|
||||
queryNode as {
|
||||
__meta__?: { deferredFields: Record<string, (keyof TFrag)[]> };
|
||||
}
|
||||
).__meta__?.deferredFields;
|
||||
|
||||
if (!deferredFields) return true;
|
||||
|
||||
const fragDef = fragmentNode.definitions[0] as
|
||||
| FragmentDefinitionNode
|
||||
| undefined;
|
||||
const fragName = fragDef?.name?.value;
|
||||
|
||||
const fields = (fragName && deferredFields[fragName]) || [];
|
||||
return fields.length > 0 && fields.every((field) => data && field in data);
|
||||
}
|
||||
51
examples/guides/codegen-nhost/src/lib/graphql/__generated__/gql.ts
generated
Normal file
51
examples/guides/codegen-nhost/src/lib/graphql/__generated__/gql.ts
generated
Normal file
@@ -0,0 +1,51 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import type { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core";
|
||||
import * as types from "./graphql";
|
||||
|
||||
/**
|
||||
* Map of all GraphQL operations in the project.
|
||||
*
|
||||
* This map has several performance disadvantages:
|
||||
* 1. It is not tree-shakeable, so it will include all operations in the project.
|
||||
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
|
||||
* 3. It does not support dead code elimination, so it will add unused operations.
|
||||
*
|
||||
* Therefore it is highly recommended to use the babel or swc plugin for production.
|
||||
* Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
|
||||
*/
|
||||
type Documents = {
|
||||
"query GetNinjaTurtlesWithComments {\n ninjaTurtles {\n id\n name\n description\n createdAt\n updatedAt\n comments {\n id\n comment\n createdAt\n user {\n id\n displayName\n email\n }\n }\n }\n}\n\nmutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {\n insertComment(object: {ninjaTurtleId: $ninjaTurtleId, comment: $comment}) {\n id\n comment\n createdAt\n ninjaTurtleId\n }\n}": typeof types.GetNinjaTurtlesWithCommentsDocument;
|
||||
};
|
||||
const documents: Documents = {
|
||||
"query GetNinjaTurtlesWithComments {\n ninjaTurtles {\n id\n name\n description\n createdAt\n updatedAt\n comments {\n id\n comment\n createdAt\n user {\n id\n displayName\n email\n }\n }\n }\n}\n\nmutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {\n insertComment(object: {ninjaTurtleId: $ninjaTurtleId, comment: $comment}) {\n id\n comment\n createdAt\n ninjaTurtleId\n }\n}":
|
||||
types.GetNinjaTurtlesWithCommentsDocument,
|
||||
};
|
||||
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
|
||||
* ```
|
||||
*
|
||||
* The query argument is unknown!
|
||||
* Please regenerate the types.
|
||||
*/
|
||||
export function graphql(source: string): unknown;
|
||||
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
export function graphql(
|
||||
source: "query GetNinjaTurtlesWithComments {\n ninjaTurtles {\n id\n name\n description\n createdAt\n updatedAt\n comments {\n id\n comment\n createdAt\n user {\n id\n displayName\n email\n }\n }\n }\n}\n\nmutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {\n insertComment(object: {ninjaTurtleId: $ninjaTurtleId, comment: $comment}) {\n id\n comment\n createdAt\n ninjaTurtleId\n }\n}",
|
||||
): (typeof documents)["query GetNinjaTurtlesWithComments {\n ninjaTurtles {\n id\n name\n description\n createdAt\n updatedAt\n comments {\n id\n comment\n createdAt\n user {\n id\n displayName\n email\n }\n }\n }\n}\n\nmutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {\n insertComment(object: {ninjaTurtleId: $ninjaTurtleId, comment: $comment}) {\n id\n comment\n createdAt\n ninjaTurtleId\n }\n}"];
|
||||
|
||||
export function graphql(source: string) {
|
||||
return (documents as any)[source] ?? {};
|
||||
}
|
||||
|
||||
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> =
|
||||
TDocumentNode extends DocumentNode<infer TType, any> ? TType : never;
|
||||
7026
examples/guides/codegen-nhost/src/lib/graphql/__generated__/graphql.ts
generated
Normal file
7026
examples/guides/codegen-nhost/src/lib/graphql/__generated__/graphql.ts
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
examples/guides/codegen-nhost/src/lib/graphql/__generated__/index.ts
generated
Normal file
2
examples/guides/codegen-nhost/src/lib/graphql/__generated__/index.ts
generated
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./fragment-masking";
|
||||
export * from "./gql";
|
||||
@@ -0,0 +1,28 @@
|
||||
query GetNinjaTurtlesWithComments {
|
||||
ninjaTurtles {
|
||||
id
|
||||
name
|
||||
description
|
||||
createdAt
|
||||
updatedAt
|
||||
comments {
|
||||
id
|
||||
comment
|
||||
createdAt
|
||||
user {
|
||||
id
|
||||
displayName
|
||||
email
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutation AddComment($ninjaTurtleId: uuid!, $comment: String!) {
|
||||
insertComment(object: { ninjaTurtleId: $ninjaTurtleId, comment: $comment }) {
|
||||
id
|
||||
comment
|
||||
createdAt
|
||||
ninjaTurtleId
|
||||
}
|
||||
}
|
||||
175
examples/guides/codegen-nhost/src/lib/nhost/AuthProvider.tsx
Normal file
175
examples/guides/codegen-nhost/src/lib/nhost/AuthProvider.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import { createClient, type NhostClient } from "@nhost/nhost-js";
|
||||
import type { Session } from "@nhost/nhost-js/auth";
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
/**
|
||||
* Authentication context interface providing access to user session state and Nhost client.
|
||||
* Used throughout the React application to access authentication-related data and operations.
|
||||
*/
|
||||
interface AuthContextType {
|
||||
/** Current authenticated user object, null if not authenticated */
|
||||
user: Session["user"] | null;
|
||||
/** Current session object containing tokens and user data, null if no active session */
|
||||
session: Session | null;
|
||||
/** Boolean indicating if user is currently authenticated */
|
||||
isAuthenticated: boolean;
|
||||
/** Boolean indicating if authentication state is still loading */
|
||||
isLoading: boolean;
|
||||
/** Nhost client instance for making authenticated requests */
|
||||
nhost: NhostClient;
|
||||
}
|
||||
|
||||
// Create React context for authentication state and nhost client
|
||||
const AuthContext = createContext<AuthContextType | null>(null);
|
||||
|
||||
interface AuthProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* AuthProvider component that provides authentication context to the React application.
|
||||
*
|
||||
* This component handles:
|
||||
* - Initializing the Nhost client with default EventEmitterStorage
|
||||
* - Managing authentication state (user, session, loading, authenticated status)
|
||||
* - Cross-tab session synchronization using sessionStorage.onChange events
|
||||
* - Page visibility and focus event handling to maintain session consistency
|
||||
* - Client-side only session management (no server-side rendering)
|
||||
*/
|
||||
export const AuthProvider = ({ children }: AuthProviderProps) => {
|
||||
const [user, setUser] = useState<Session["user"] | null>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const lastRefreshTokenIdRef = useRef<string | null>(null);
|
||||
|
||||
// Initialize Nhost client with default SessionStorage (local storage)
|
||||
const nhost = useMemo(
|
||||
() =>
|
||||
createClient({
|
||||
region: import.meta.env.VITE_NHOST_REGION || "local",
|
||||
subdomain: import.meta.env.VITE_NHOST_SUBDOMAIN || "local",
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles session reload when refresh token changes.
|
||||
* This detects when the session has been updated from other tabs.
|
||||
* Unlike the Next.js version, this only updates local state without server synchronization.
|
||||
*
|
||||
* @param currentRefreshTokenId - The current refresh token ID to compare against stored value
|
||||
*/
|
||||
const reloadSession = useCallback(
|
||||
(currentRefreshTokenId: string | null) => {
|
||||
if (currentRefreshTokenId !== lastRefreshTokenIdRef.current) {
|
||||
lastRefreshTokenIdRef.current = currentRefreshTokenId;
|
||||
|
||||
// Update local authentication state to match current session
|
||||
const currentSession = nhost.getUserSession();
|
||||
setUser(currentSession?.user || null);
|
||||
setSession(currentSession);
|
||||
setIsAuthenticated(!!currentSession);
|
||||
}
|
||||
},
|
||||
[nhost],
|
||||
);
|
||||
|
||||
// Initialize authentication state and set up cross-tab session synchronization
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
|
||||
// Load initial session state from Nhost client
|
||||
const currentSession = nhost.getUserSession();
|
||||
setUser(currentSession?.user || null);
|
||||
setSession(currentSession);
|
||||
setIsAuthenticated(!!currentSession);
|
||||
lastRefreshTokenIdRef.current = currentSession?.refreshTokenId ?? null;
|
||||
setIsLoading(false);
|
||||
|
||||
// Subscribe to session changes from other browser tabs
|
||||
// This enables real-time synchronization when user signs in/out in another tab
|
||||
const unsubscribe = nhost.sessionStorage.onChange((session) => {
|
||||
reloadSession(session?.refreshTokenId ?? null);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [nhost, reloadSession]);
|
||||
|
||||
// Handle session changes from page focus events (for additional session consistency)
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Checks for session changes when page becomes visible or focused.
|
||||
* In the React SPA context, this provides additional consistency checks
|
||||
* though it's less critical than in the Next.js SSR version.
|
||||
*/
|
||||
const checkSessionOnFocus = () => {
|
||||
reloadSession(nhost.getUserSession()?.refreshTokenId ?? null);
|
||||
};
|
||||
|
||||
// Monitor page visibility changes (tab switching, window minimizing)
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden) {
|
||||
checkSessionOnFocus();
|
||||
}
|
||||
});
|
||||
|
||||
// Monitor window focus events (clicking back into the browser window)
|
||||
window.addEventListener("focus", checkSessionOnFocus);
|
||||
|
||||
// Cleanup event listeners on component unmount
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", checkSessionOnFocus);
|
||||
window.removeEventListener("focus", checkSessionOnFocus);
|
||||
};
|
||||
}, [nhost, reloadSession]);
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
session,
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
nhost,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to access the authentication context.
|
||||
*
|
||||
* Must be used within a component wrapped by AuthProvider.
|
||||
* Provides access to current user session, authentication state, and Nhost client.
|
||||
*
|
||||
* @throws {Error} When used outside of AuthProvider
|
||||
* @returns {AuthContextType} Authentication context containing user, session, and client
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function MyComponent() {
|
||||
* const { user, isAuthenticated, nhost } = useAuth();
|
||||
*
|
||||
* if (!isAuthenticated) {
|
||||
* return <div>Please sign in</div>;
|
||||
* }
|
||||
*
|
||||
* return <div>Welcome, {user?.displayName}!</div>;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
19
examples/guides/codegen-nhost/src/main.tsx
Normal file
19
examples/guides/codegen-nhost/src/main.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import { AuthProvider } from "./lib/nhost/AuthProvider";
|
||||
|
||||
// Root component that sets up providers
|
||||
const Root = () => (
|
||||
<React.StrictMode>
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
const rootElement = document.getElementById("root");
|
||||
if (!rootElement) throw new Error("Root element not found");
|
||||
|
||||
createRoot(rootElement).render(<Root />);
|
||||
217
examples/guides/codegen-nhost/src/pages/Home.css
Normal file
217
examples/guides/codegen-nhost/src/pages/Home.css
Normal file
@@ -0,0 +1,217 @@
|
||||
/* Custom styles for Ninja Turtles tabs interface */
|
||||
.ninja-turtles-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.ninja-turtles-title {
|
||||
text-align: center;
|
||||
margin-bottom: 25px;
|
||||
color: #1a9c44;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
/* Tab navigation */
|
||||
.turtle-tabs {
|
||||
display: flex;
|
||||
border-bottom: 2px solid #1a9c44;
|
||||
margin-bottom: 20px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.turtle-tab {
|
||||
padding: 10px 20px;
|
||||
margin-right: 5px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.3s ease;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
position: relative;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.turtle-tab:hover {
|
||||
color: var(--text-primary);
|
||||
background: rgba(26, 156, 68, 0.1);
|
||||
}
|
||||
|
||||
.turtle-tab.active {
|
||||
color: white;
|
||||
background: #1a9c44;
|
||||
}
|
||||
|
||||
/* Turtle Card Styles */
|
||||
.turtle-card {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
animation: fadeIn 0.5s;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.turtle-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.turtle-name {
|
||||
color: #1a9c44;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.turtle-description {
|
||||
margin-bottom: 20px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.turtle-date {
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Comments section */
|
||||
.comments-section {
|
||||
margin-top: 25px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.comments-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 15px;
|
||||
color: #1a9c44;
|
||||
}
|
||||
|
||||
.comment-card {
|
||||
margin-bottom: 15px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(26, 156, 68, 0.05);
|
||||
border: 1px solid rgba(26, 156, 68, 0.1);
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.comment-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.comment-avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background-color: #1a9c44;
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75rem;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Comment form */
|
||||
.comment-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.comment-textarea {
|
||||
width: 100%;
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
color: white;
|
||||
transition: all 0.2s;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.comment-textarea:focus {
|
||||
border-color: #1a9c44;
|
||||
box-shadow: 0 0 0 2px rgba(26, 156, 68, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.comment-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background-color: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.cancel-button:hover {
|
||||
background-color: rgba(31, 41, 55, 0.8);
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: #1a9c44;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: #148035;
|
||||
}
|
||||
|
||||
.add-comment-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: #1a9c44;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 5px 0;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.add-comment-button:hover {
|
||||
color: #148035;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.add-comment-button svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 640px) {
|
||||
.turtle-tabs {
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.turtle-tab {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
237
examples/guides/codegen-nhost/src/pages/Home.tsx
Normal file
237
examples/guides/codegen-nhost/src/pages/Home.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import { type JSX, useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
AddCommentDocument,
|
||||
GetNinjaTurtlesWithCommentsDocument,
|
||||
type GetNinjaTurtlesWithCommentsQuery,
|
||||
} from "../lib/graphql/__generated__/graphql";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
import "./Home.css";
|
||||
|
||||
export default function Home(): JSX.Element {
|
||||
const { isLoading, nhost } = useAuth();
|
||||
const [activeCommentId, setActiveCommentId] = useState<string | null>(null);
|
||||
const [commentText, setCommentText] = useState("");
|
||||
const [activeTabId, setActiveTabId] = useState<string | null>(null);
|
||||
|
||||
const [data, setData] = useState<GetNinjaTurtlesWithCommentsQuery | null>(
|
||||
null,
|
||||
);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
|
||||
// Fetch ninja turtles data
|
||||
const fetchNinjaTurtles = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const result = await nhost.graphql.request(
|
||||
GetNinjaTurtlesWithCommentsDocument,
|
||||
{},
|
||||
);
|
||||
|
||||
if (result.body.errors) {
|
||||
throw new Error(result.body.errors[0]?.message);
|
||||
}
|
||||
|
||||
setData(result.body.data ?? null);
|
||||
} catch (err) {
|
||||
setError(err as Error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [nhost.graphql]);
|
||||
|
||||
// Load data on mount
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
fetchNinjaTurtles();
|
||||
}
|
||||
}, [isLoading, fetchNinjaTurtles]);
|
||||
|
||||
const addComment = async (ninjaTurtleId: string, comment: string) => {
|
||||
try {
|
||||
const result = await nhost.graphql.request(AddCommentDocument, {
|
||||
ninjaTurtleId,
|
||||
comment,
|
||||
});
|
||||
|
||||
if (result.body.errors) {
|
||||
throw new Error(result.body.errors[0]?.message);
|
||||
}
|
||||
|
||||
// Clear form and refetch data
|
||||
setCommentText("");
|
||||
setActiveCommentId(null);
|
||||
await fetchNinjaTurtles();
|
||||
} catch (err) {
|
||||
console.error("Error adding comment:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// If authentication is still loading, show a loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleAddComment = (turtleId: string) => {
|
||||
if (!commentText.trim()) return;
|
||||
|
||||
addComment(turtleId, commentText);
|
||||
};
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<p>Loading ninja turtles...</p>
|
||||
</div>
|
||||
);
|
||||
if (error)
|
||||
return (
|
||||
<div className="alert alert-error">
|
||||
Error loading ninja turtles: {(error as Error).message}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Access the data using the correct field name from the GraphQL response
|
||||
const ninjaTurtles = data?.ninjaTurtles || [];
|
||||
if (!ninjaTurtles || ninjaTurtles.length === 0) {
|
||||
return (
|
||||
<div className="no-turtles-container">
|
||||
<p>No ninja turtles found. Please add some!</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Set the active tab to the first turtle if there's no active tab and there are turtles
|
||||
if (activeTabId === null) {
|
||||
setActiveTabId(ninjaTurtles[0] ? ninjaTurtles[0].id : null);
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="ninja-turtles-container">
|
||||
<h1 className="ninja-turtles-title text-3xl font-bold mb-6">
|
||||
Teenage Mutant Ninja Turtles
|
||||
</h1>
|
||||
|
||||
{/* Tabs navigation */}
|
||||
<div className="turtle-tabs">
|
||||
{ninjaTurtles.map((turtle) => (
|
||||
<button
|
||||
key={turtle.id}
|
||||
type="button"
|
||||
className={`turtle-tab ${activeTabId === turtle.id ? "active" : ""}`}
|
||||
onClick={() => setActiveTabId(turtle.id)}
|
||||
>
|
||||
{turtle.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Display active turtle */}
|
||||
{ninjaTurtles
|
||||
.filter((turtle) => turtle.id === activeTabId)
|
||||
.map((turtle) => (
|
||||
<div key={turtle.id} className="turtle-card glass-card p-6">
|
||||
<div className="turtle-header">
|
||||
<h2 className="turtle-name text-2xl font-semibold">
|
||||
{turtle.name}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p className="turtle-description">{turtle.description}</p>
|
||||
|
||||
<div className="turtle-date">
|
||||
Added on {formatDate(turtle.createdAt || turtle.createdAt)}
|
||||
</div>
|
||||
|
||||
<div className="comments-section">
|
||||
<h3 className="comments-title">
|
||||
Comments ({turtle.comments.length})
|
||||
</h3>
|
||||
|
||||
{turtle.comments.map((comment) => (
|
||||
<div key={comment.id} className="comment-card">
|
||||
<p className="comment-text">{comment.comment}</p>
|
||||
<div className="comment-meta">
|
||||
<div className="comment-avatar">
|
||||
{(comment.user?.displayName || comment.user?.email || "?")
|
||||
.charAt(0)
|
||||
.toUpperCase()}
|
||||
</div>
|
||||
<p>
|
||||
{comment.user?.displayName ||
|
||||
comment.user?.email ||
|
||||
"Anonymous"}{" "}
|
||||
- {formatDate(comment.createdAt || comment.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{activeCommentId === turtle.id ? (
|
||||
<div className="comment-form">
|
||||
<textarea
|
||||
className="comment-textarea"
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
placeholder="Add your comment..."
|
||||
rows={3}
|
||||
/>
|
||||
<div className="comment-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveCommentId(null);
|
||||
setCommentText("");
|
||||
}}
|
||||
className="btn cancel-button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAddComment(turtle.id)}
|
||||
className="btn submit-button"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setActiveCommentId(turtle.id)}
|
||||
className="add-comment-button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-label="Add Comment"
|
||||
role="img"
|
||||
>
|
||||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
</svg>
|
||||
Add a comment
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
examples/guides/codegen-nhost/src/pages/Profile.tsx
Normal file
66
examples/guides/codegen-nhost/src/pages/Profile.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import type { JSX } from "react";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function Profile(): JSX.Element {
|
||||
const { user, session } = useAuth();
|
||||
|
||||
// ProtectedRoute component now handles authentication check
|
||||
// We can just focus on the component logic here
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-3xl mb-6 gradient-text">Your Profile</h1>
|
||||
|
||||
<div className="glass-card p-8 mb-6">
|
||||
<div className="space-y-5">
|
||||
<div className="profile-item">
|
||||
<strong>Display Name:</strong>
|
||||
<span className="ml-2">{user?.displayName || "Not set"}</span>
|
||||
</div>
|
||||
|
||||
<div className="profile-item">
|
||||
<strong>Email:</strong>
|
||||
<span className="ml-2">{user?.email || "Not available"}</span>
|
||||
</div>
|
||||
|
||||
<div className="profile-item">
|
||||
<strong>User ID:</strong>
|
||||
<span
|
||||
className="ml-2"
|
||||
style={{
|
||||
fontFamily: "var(--font-geist-mono)",
|
||||
fontSize: "0.875rem",
|
||||
}}
|
||||
>
|
||||
{user?.id || "Not available"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="profile-item">
|
||||
<strong>Roles:</strong>
|
||||
<span className="ml-2">{user?.roles?.join(", ") || "None"}</span>
|
||||
</div>
|
||||
|
||||
<div className="profile-item">
|
||||
<strong>Email Verified:</strong>
|
||||
<span className="ml-2">{user?.emailVerified ? "Yes" : "No"}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="glass-card p-8 mb-6">
|
||||
<h3 className="text-xl mb-4">Session Information</h3>
|
||||
<pre>
|
||||
{JSON.stringify(
|
||||
{
|
||||
refreshTokenId: session?.refreshTokenId,
|
||||
accessTokenExpiresIn: session?.accessTokenExpiresIn,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
120
examples/guides/codegen-nhost/src/pages/SignIn.tsx
Normal file
120
examples/guides/codegen-nhost/src/pages/SignIn.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import type { ErrorResponse } from "@nhost/nhost-js/auth";
|
||||
import type { FetchError } from "@nhost/nhost-js/fetch";
|
||||
import { type JSX, useEffect, useId, useState } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function SignIn(): JSX.Element {
|
||||
const { nhost, isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const params = new URLSearchParams(location.search);
|
||||
const emailId = useId();
|
||||
const passwordId = useId();
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(
|
||||
params.get("error") || null,
|
||||
);
|
||||
|
||||
const isVerifying = params.has("fromVerify");
|
||||
|
||||
// Use useEffect for navigation after authentication is confirmed
|
||||
useEffect(() => {
|
||||
if (isAuthenticated && !isVerifying) {
|
||||
navigate("/home");
|
||||
}
|
||||
}, [isAuthenticated, isVerifying, navigate]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Use the signIn function from auth context
|
||||
const response = await nhost.auth.signInEmailPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
// Check if MFA is required
|
||||
if (response.body?.mfa) {
|
||||
navigate(`/signin/mfa?ticket=${response.body.mfa.ticket}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we have a session, sign in was successful
|
||||
if (response.body?.session) {
|
||||
navigate("/home");
|
||||
} else {
|
||||
setError("Failed to sign in");
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as FetchError<ErrorResponse>;
|
||||
setError(`An error occurred during sign in: ${error.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="text-3xl mb-6 gradient-text">Nhost SDK Demo</h1>
|
||||
|
||||
<div className="glass-card w-full p-8 mb-6">
|
||||
<h2 className="text-2xl mb-6">Sign In</h2>
|
||||
<div>
|
||||
<div className="tabs-container">
|
||||
<button type="button" className="tab-button tab-active">
|
||||
Email + Password
|
||||
</button>
|
||||
</div>
|
||||
<div className="tab-content">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label htmlFor={emailId}>Email</label>
|
||||
<input
|
||||
id={emailId}
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor={passwordId}>Password</label>
|
||||
<input
|
||||
id={passwordId}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Signing In..." : "Sign In"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<p>
|
||||
Don't have an account? <Link to="/signup">Sign Up</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
127
examples/guides/codegen-nhost/src/pages/SignUp.tsx
Normal file
127
examples/guides/codegen-nhost/src/pages/SignUp.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import type { ErrorResponse } from "@nhost/nhost-js/auth";
|
||||
import type { FetchError } from "@nhost/nhost-js/fetch";
|
||||
import { type JSX, useId, useState } from "react";
|
||||
import { Link, Navigate, useNavigate } from "react-router-dom";
|
||||
import { useAuth } from "../lib/nhost/AuthProvider";
|
||||
|
||||
export default function SignUp(): JSX.Element {
|
||||
const { nhost, isAuthenticated } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const displayNameId = useId();
|
||||
const emailId = useId();
|
||||
const passwordId = useId();
|
||||
|
||||
const [email, setEmail] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [displayName, setDisplayName] = useState<string>("");
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// If already authenticated, redirect to profile
|
||||
if (isAuthenticated) {
|
||||
return <Navigate to="/home" />;
|
||||
}
|
||||
|
||||
const handleSubmit = async (
|
||||
e: React.FormEvent<HTMLFormElement>,
|
||||
): Promise<void> => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await nhost.auth.signUpEmailPassword({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
displayName,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.body) {
|
||||
// Successfully signed up and automatically signed in
|
||||
navigate("/home");
|
||||
} else {
|
||||
// Verification email sent
|
||||
navigate("/verify");
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as FetchError<ErrorResponse>;
|
||||
setError(`An error occurred during sign up: ${error.message}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<h1 className="text-3xl mb-6 gradient-text">Nhost SDK Demo</h1>
|
||||
|
||||
<div className="glass-card w-full p-8 mb-6">
|
||||
<h2 className="text-2xl mb-6">Sign Up</h2>
|
||||
|
||||
<div>
|
||||
<div className="tabs-container">
|
||||
<button type="button" className="tab-button tab-active">
|
||||
Email + Password
|
||||
</button>
|
||||
</div>
|
||||
<div className="tab-content">
|
||||
<form onSubmit={handleSubmit} className="space-y-5">
|
||||
<div>
|
||||
<label htmlFor={displayNameId}>Display Name</label>
|
||||
<input
|
||||
id={displayNameId}
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor={emailId}>Email</label>
|
||||
<input
|
||||
id={emailId}
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor={passwordId}>Password</label>
|
||||
<input
|
||||
id={passwordId}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<p className="text-xs mt-1 text-gray-400">
|
||||
Password must be at least 8 characters long
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? "Signing Up..." : "Sign Up"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<p>
|
||||
Already have an account? <Link to="/signin">Sign In</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
examples/guides/codegen-nhost/src/vite-env.d.ts
vendored
Normal file
11
examples/guides/codegen-nhost/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_NHOST_REGION: string | undefined;
|
||||
readonly VITE_NHOST_SUBDOMAIN: string | undefined;
|
||||
readonly VITE_ENV: string | undefined;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
6
examples/guides/codegen-nhost/tsconfig.json
Normal file
6
examples/guides/codegen-nhost/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../build/configs/tsconfig/frontend.json",
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
4
examples/guides/codegen-nhost/tsconfig.node.json
Normal file
4
examples/guides/codegen-nhost/tsconfig.node.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../../build/configs/tsconfig/vite.json"
|
||||
}
|
||||
7
examples/guides/codegen-nhost/vite.config.ts
Normal file
7
examples/guides/codegen-nhost/vite.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
@@ -17,6 +17,8 @@ let
|
||||
"pnpm-lock.yaml"
|
||||
"${submodule}/package.json"
|
||||
"${submodule}/pnpm-lock.yaml"
|
||||
"${submodule}/codegen-nhost/package.json"
|
||||
"${submodule}/codegen-nhost/pnpm-lock.yaml"
|
||||
"${submodule}/react-apollo/package.json"
|
||||
"${submodule}/react-apollo/pnpm-lock.yaml"
|
||||
"${submodule}/react-query/package.json"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "demos/react-apollo",
|
||||
"name": "guides/react-apollo",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "demos/react-query",
|
||||
"name": "guides/react-query",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "demos/react-urql",
|
||||
"name": "guides/react-urql",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"private": true,
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"js-yaml@<=4.1.0": ">=4.1.1"
|
||||
"js-yaml@<=4.1.0": ">=4.1.1",
|
||||
"glob@>=10.3.7 <=11.0.3": ">=11.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ settings:
|
||||
|
||||
overrides:
|
||||
js-yaml@<=4.1.0: '>=4.1.1'
|
||||
glob@>=10.3.7 <=11.0.3: '>=11.1.0'
|
||||
|
||||
importers:
|
||||
|
||||
@@ -699,6 +700,14 @@ packages:
|
||||
resolution: {integrity: sha512-ReZxZ8pdnoI3tP/dNnJdnmAk7uLT4FjsKDGW7YeDdvdOMz2XCQSmSCM9IWlrXuWtMF9zeSB6WJtEhCQ41gQOfw==}
|
||||
hasBin: true
|
||||
|
||||
'@isaacs/balanced-match@4.0.1':
|
||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -762,10 +771,6 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@radix-ui/primitive@1.1.3':
|
||||
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
|
||||
|
||||
@@ -1847,8 +1852,9 @@ packages:
|
||||
resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
glob@10.4.5:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
glob@12.0.0:
|
||||
resolution: {integrity: sha512-5Qcll1z7IKgHr5g485ePDdHcNQY0k2dtv/bjYy0iuyGxQw2qSOiiXUXJ+AYQpg3HNoUMHqAruX478Jeev7UULw==}
|
||||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
glob@7.2.3:
|
||||
@@ -1977,8 +1983,9 @@ packages:
|
||||
resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
jackspeak@4.1.1:
|
||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
jest-environment-node@29.7.0:
|
||||
resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==}
|
||||
@@ -2154,6 +2161,10 @@ packages:
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@11.2.2:
|
||||
resolution: {integrity: sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
@@ -2318,6 +2329,10 @@ packages:
|
||||
resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
minimatch@10.1.1:
|
||||
resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
@@ -2485,9 +2500,9 @@ packages:
|
||||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
path-scurry@2.0.1:
|
||||
resolution: {integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
picocolors@1.1.1:
|
||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||
@@ -3842,7 +3857,7 @@ snapshots:
|
||||
expo: 54.0.9(@babel/core@7.28.4)(@expo/metro-runtime@6.1.2)(expo-router@6.0.7)(react-native@0.81.4(@babel/core@7.28.4)(@types/react@19.1.13)(react@19.1.0))(react@19.1.0)
|
||||
freeport-async: 2.0.0
|
||||
getenv: 2.0.0
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
lan-network: 0.1.7
|
||||
minimatch: 9.0.5
|
||||
node-forge: 1.3.1
|
||||
@@ -3894,7 +3909,7 @@ snapshots:
|
||||
chalk: 4.1.2
|
||||
debug: 4.4.3
|
||||
getenv: 2.0.0
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
resolve-from: 5.0.0
|
||||
semver: 7.7.2
|
||||
slash: 3.0.0
|
||||
@@ -3914,7 +3929,7 @@ snapshots:
|
||||
'@expo/json-file': 10.0.7
|
||||
deepmerge: 4.3.1
|
||||
getenv: 2.0.0
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
require-from-string: 2.0.2
|
||||
resolve-from: 5.0.0
|
||||
resolve-workspace-root: 2.0.0
|
||||
@@ -3928,7 +3943,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@expo/sudo-prompt': 9.3.2
|
||||
debug: 3.2.7
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -3956,7 +3971,7 @@ snapshots:
|
||||
chalk: 4.1.2
|
||||
debug: 4.4.3
|
||||
getenv: 2.0.0
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
ignore: 5.3.2
|
||||
minimatch: 9.0.5
|
||||
p-limit: 3.1.0
|
||||
@@ -4008,7 +4023,7 @@ snapshots:
|
||||
dotenv: 16.4.7
|
||||
dotenv-expand: 11.0.7
|
||||
getenv: 2.0.0
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
hermes-parser: 0.29.1
|
||||
jsc-safe-url: 0.2.4
|
||||
lightningcss: 1.30.1
|
||||
@@ -4121,6 +4136,12 @@ snapshots:
|
||||
find-up: 5.0.0
|
||||
js-yaml: 4.1.1
|
||||
|
||||
'@isaacs/balanced-match@4.0.1': {}
|
||||
|
||||
'@isaacs/brace-expansion@5.0.0':
|
||||
dependencies:
|
||||
'@isaacs/balanced-match': 4.0.1
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
@@ -4223,9 +4244,6 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/primitive@1.1.3': {}
|
||||
|
||||
'@radix-ui/react-collection@1.1.7(@types/react@19.1.13)(react-dom@19.1.1(react@19.1.0))(react@19.1.0)':
|
||||
@@ -5216,7 +5234,7 @@ snapshots:
|
||||
'@expo/spawn-async': 1.7.2
|
||||
chalk: 4.1.2
|
||||
commander: 7.2.0
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
require-from-string: 2.0.2
|
||||
resolve-from: 5.0.0
|
||||
|
||||
@@ -5380,14 +5398,14 @@ snapshots:
|
||||
|
||||
getenv@2.0.0: {}
|
||||
|
||||
glob@10.4.5:
|
||||
glob@12.0.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
jackspeak: 4.1.1
|
||||
minimatch: 10.1.1
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
path-scurry: 2.0.1
|
||||
|
||||
glob@7.2.3:
|
||||
dependencies:
|
||||
@@ -5507,11 +5525,9 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
jackspeak@3.4.3:
|
||||
jackspeak@4.1.1:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jest-environment-node@29.7.0:
|
||||
dependencies:
|
||||
@@ -5685,6 +5701,8 @@ snapshots:
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@11.2.2: {}
|
||||
|
||||
lru-cache@5.1.1:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
@@ -6075,6 +6093,10 @@ snapshots:
|
||||
|
||||
mimic-fn@1.2.0: {}
|
||||
|
||||
minimatch@10.1.1:
|
||||
dependencies:
|
||||
'@isaacs/brace-expansion': 5.0.0
|
||||
|
||||
minimatch@3.1.2:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
@@ -6217,9 +6239,9 @@ snapshots:
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
path-scurry@2.0.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
lru-cache: 11.2.2
|
||||
minipass: 7.1.2
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
@@ -6630,7 +6652,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
commander: 4.1.1
|
||||
glob: 10.4.5
|
||||
glob: 12.0.0
|
||||
lines-and-columns: 1.2.4
|
||||
mz: 2.7.0
|
||||
pirates: 4.0.7
|
||||
|
||||
13
go.mod
13
go.mod
@@ -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
66
go.sum
@@ -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=
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`},
|
||||
|
||||
@@ -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"`},
|
||||
|
||||
@@ -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"`},
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
BIN
services/storage/image/testdata/nhost.heic
vendored
Normal file
BIN
services/storage/image/testdata/nhost.heic
vendored
Normal file
Binary file not shown.
21
vendor/github.com/cshum/vipsgen/LICENSE
generated
vendored
Normal file
21
vendor/github.com/cshum/vipsgen/LICENSE
generated
vendored
Normal 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
100
vendor/github.com/cshum/vipsgen/pointer/pointer.go
generated
vendored
Normal 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
76
vendor/github.com/cshum/vipsgen/vips/callback.go
generated
vendored
Normal 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
48
vendor/github.com/cshum/vipsgen/vips/connection.c
generated
vendored
Normal 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
97
vendor/github.com/cshum/vipsgen/vips/connection.go
generated
vendored
Normal 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
Reference in New Issue
Block a user