diff --git a/.github/workflows/cypress-test.yml b/.github/workflows/cypress-test.yml index 9e95bec28..0e38ced52 100644 --- a/.github/workflows/cypress-test.yml +++ b/.github/workflows/cypress-test.yml @@ -19,6 +19,20 @@ jobs: ports: - 3306:3306 options: --health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -proot" --health-interval=10s --health-timeout=5s --health-retries=10 + + postgres: + image: postgres:16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: fireback_test + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U postgres -d fireback_test" + --health-interval=10s + --health-timeout=5s + --health-retries=10 steps: - name: Checkout code @@ -48,27 +62,8 @@ jobs: - name: Install Fireback run: sudo dpkg -i artifacts-ubuntu/fireback-server-all/fireback-amd64.deb - # - name: Setup Database - # run: | - # if [[ "${{ inputs.DB_TYPE }}" == "mysql" ]]; then - # fireback config db-dsn set root:root@tcp(localhost:3306)/fireback_test?charset=utf8mb4&parseTime=True&loc=Local - # fireback config db-vendor set mysql - # else - # fireback config db-name /tmp/database.db && \ - # fireback config db-vendor set sqlite - # fi - # fireback migration apply - - - # - name: Add admin account - # run: fireback auth --in-root=true --value admin --workspace-type-id root --type email --password admin --first-name Ali --last-name Torabi - # - name: Check the passport - # run: fireback passport check-passport-methods - - - - name: Run Cypress tests - run: cd e2e && DB_TYPE=${{ inputs.DB_TYPE }} npm test + run: cd e2e && DB_TYPE=${{ inputs.DB_TYPE }} npm test - name: Change the fireback replacement to github actions run: cd e2e/samples/fireback-data-types && sed -i 's|/Users/ali/work/fireback|/home/runner/work/fireback/fireback|' go.mod diff --git a/.github/workflows/fireback-build.yml b/.github/workflows/fireback-build.yml index 3a172bf8f..c6ce14354 100644 --- a/.github/workflows/fireback-build.yml +++ b/.github/workflows/fireback-build.yml @@ -638,6 +638,13 @@ jobs: uses: ./.github/workflows/cypress-test.yml with: DB_TYPE: mysql + + run-cypress-postgres: + needs: + - build-ubuntu + uses: ./.github/workflows/cypress-test.yml + with: + DB_TYPE: postgres run-cypress-sqlite: needs: diff --git a/e2e/cypress.config.js b/e2e/cypress.config.js index 93a472928..d723a403d 100644 --- a/e2e/cypress.config.js +++ b/e2e/cypress.config.js @@ -5,7 +5,7 @@ let firebackProcess; // Store the Fireback process reference let BINARY = "/home/ali/work/fireback/app"; let CWD = "/home/ali/work/fireback"; -const PORT = 7793; +const PORT = 7794; let DB_VENDOR = "sqlite"; const isGitHubActions = !!process.env.GITHUB_ACTIONS; @@ -62,7 +62,7 @@ module.exports = defineConfig({ await execAsync(`${BINARY} config db-vendor set mysql`, CWD); await execAsync( `${BINARY} config db-dsn set "root:root@tcp(localhost:3306)/fireback_test?charset=utf8mb4&parseTime=True&loc=Local"`, - CWD + CWD, ); await execAsync(`${BINARY} migration apply`, CWD); @@ -70,11 +70,42 @@ module.exports = defineConfig({ } catch (err) { console.error("setup mysql failed:", err); } + } else if (vendor === "postgres") { + try { + console.log("Using postgres"); + await execAsync(`${BINARY} config db-vendor set postgres`, CWD); + + const dbName = "fireback_test"; + + // Drop and recreate database + console.log( + await execAsync( + `PGPASSWORD=postgres psql -U postgres -h localhost -p 5432 -d postgres -c "DROP DATABASE IF EXISTS ${dbName};"`, + CWD, + ), + ); + + await execAsync( + `PGPASSWORD=postgres psql -U postgres -h localhost -p 5432 -d postgres -c "CREATE DATABASE ${dbName};"`, + CWD, + ); + + await execAsync( + `${BINARY} config db-dsn set "host=localhost user=postgres password=postgres dbname=${dbName} port=5432 sslmode=disable TimeZone=UTC"`, + CWD, + ); + + await execAsync(`${BINARY} migration apply`, CWD); + + return true; + } catch (err) { + console.error("setup postgres failed:", err); + } } else { await execAsync(`${BINARY} config db-vendor set sqlite`, CWD); await execAsync( `${BINARY} config db-name set /tmp/${dbname}.db`, - CWD + CWD, ); await execAsync(`${BINARY} migration apply`, CWD); diff --git a/e2e/cypress/e2e/login.cy.js b/e2e/cypress/e2e/login.cy.js index f50da8ebc..e5b5e6a01 100644 --- a/e2e/cypress/e2e/login.cy.js +++ b/e2e/cypress/e2e/login.cy.js @@ -19,7 +19,7 @@ describe("Logging in with the signin", () => { it("get the data of the public", () => { cy.request( "GET", - "http://localhost:7793/passports/available-methods" + "http://localhost:7794/passports/available-methods", ).then((response) => { cy.task("log", response.body); expect(response.body.data.item.email).to.equal(false); @@ -38,7 +38,7 @@ describe("Logging in with the signin", () => { it("get the data of the public", () => { cy.request( "GET", - "http://localhost:7793/passports/available-methods" + "http://localhost:7794/passports/available-methods", ).then((response) => { cy.task("log", response.body); expect(response.body.data.item.email).to.equal(true); @@ -63,8 +63,9 @@ describe("Logging in with the signin", () => { it("should be able to create a role in order to assign it into the workspace type.", () => { cy.task( "exec", - ` role c --name testagentrole --capabilities "root.*"` + ` role c --name testagentrole --capabilities "root.*"`, ).then((res) => { + console.log("Role created:", res) console.log((roleId = JSON.parse(res).uniqueId)); }); }); @@ -72,7 +73,7 @@ describe("Logging in with the signin", () => { it("should be able to create a workspace name", () => { cy.task( "exec", - ` ws type c --title customer --slug customer --role-id ${roleId}` + ` ws type c --title customer --slug customer --role-id ${roleId}`, ); }); @@ -127,6 +128,7 @@ describe("Logging in with the signin", () => { it("should be able to generate back the menu items from seeder.", () => { cy.task("exec", `misc appmenu ssync`).then((content) => { + console.log(100, content); let countFilesImported = 0; for (const line of content.split("\n")) { if (line.startsWith("Success")) { @@ -184,7 +186,7 @@ describe("Logging in with the signin", () => { cy.task( "exec", - `misc appmenu q --query "unique_id = ${item.uniqueId}"` + `misc appmenu q --query "unique_id = ${item.uniqueId}"`, ).then((content) => { const res = JSON.parse(content); console.log(res); diff --git a/e2e/samples/fireback-data-types/go.sum b/e2e/samples/fireback-data-types/go.sum index 53b085103..c8a9d3240 100644 --- a/e2e/samples/fireback-data-types/go.sum +++ b/e2e/samples/fireback-data-types/go.sum @@ -111,7 +111,7 @@ cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2b cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987794e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/474420502/gcurl v1.2.1 h1:Z+bAiOWdW7hbvTuBKxzhNuCevvvLlHDvk3hoSwHD2RA= diff --git a/e2e/samples/fireback-data-types/tests/cypress.config.js b/e2e/samples/fireback-data-types/tests/cypress.config.js index 7ced31e45..848fb7f4e 100644 --- a/e2e/samples/fireback-data-types/tests/cypress.config.js +++ b/e2e/samples/fireback-data-types/tests/cypress.config.js @@ -5,7 +5,7 @@ let firebackProcess; // Store the Fireback process reference let BINARY = "/Users/ali/work/fireback/e2e/samples/fireback-data-types/app"; let CWD = "/Users/ali/work/fireback/e2e/samples/fireback-data-types"; -const PORT = 7793; +const PORT = 7794; let DB_VENDOR = "sqlite"; const isGitHubActions = !!process.env.GITHUB_ACTIONS; @@ -61,7 +61,7 @@ module.exports = defineConfig({ await execAsync(`${BINARY} config db-vendor set mysql`, CWD); await execAsync( `${BINARY} config db-dsn set "root:root@tcp(localhost:3306)/fireback_test?charset=utf8mb4&parseTime=True&loc=Local"`, - CWD + CWD, ); await execAsync(`${BINARY} migration apply`, CWD); @@ -69,11 +69,26 @@ module.exports = defineConfig({ } catch (err) { console.error("setup mysql failed:", err); } + } else if (vendor === "postgres") { + try { + await execAsync(`${BINARY} config db-vendor set postgres`, CWD); + + await execAsync( + `${BINARY} config db-dsn set "host=localhost user=postgres password=postgres dbname=fireback_test port=5432 sslmode=disable TimeZone=UTC"`, + CWD, + ); + + await execAsync(`${BINARY} migration apply`, CWD); + + return true; + } catch (err) { + console.error("setup postgres failed:", err); + } } else { await execAsync(`${BINARY} config db-vendor set sqlite`, CWD); await execAsync( `${BINARY} config db-name set /tmp/${dbname}.db`, - CWD + CWD, ); await execAsync(`${BINARY} migration apply`, CWD); diff --git a/e2e/samples/fireback-payment/go.sum b/e2e/samples/fireback-payment/go.sum index b7f20e42e..e76b7788d 100644 --- a/e2e/samples/fireback-payment/go.sum +++ b/e2e/samples/fireback-payment/go.sum @@ -111,7 +111,7 @@ cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2b cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987794e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k= github.com/Azure/azure-storage-blob-go v0.14.0/go.mod h1:SMqIBi+SuiQH32bvyjngEewEeXoPfKMgWlBDaYf6fck= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= diff --git a/e2e/samples/fireback-payment/tests/cypress.config.js b/e2e/samples/fireback-payment/tests/cypress.config.js index 6266102dc..5259bcf4d 100644 --- a/e2e/samples/fireback-payment/tests/cypress.config.js +++ b/e2e/samples/fireback-payment/tests/cypress.config.js @@ -5,7 +5,7 @@ let firebackProcess; // Store the Fireback process reference let BINARY = "/Users/ali/work/fireback/e2e/samples/fireback-payment/app"; let CWD = "/Users/ali/work/fireback/e2e/samples/fireback-payment"; -const PORT = 7793; +const PORT = 7794; let DB_VENDOR = "sqlite"; const isGitHubActions = !!process.env.GITHUB_ACTIONS; @@ -61,7 +61,7 @@ module.exports = defineConfig({ await execAsync(`${BINARY} config db-vendor set mysql`, CWD); await execAsync( `${BINARY} config db-dsn set "root:root@tcp(localhost:3306)/fireback_test?charset=utf8mb4&parseTime=True&loc=Local"`, - CWD + CWD, ); await execAsync(`${BINARY} migration apply`, CWD); @@ -69,11 +69,26 @@ module.exports = defineConfig({ } catch (err) { console.error("setup mysql failed:", err); } + } else if (vendor === "postgres") { + try { + await execAsync(`${BINARY} config db-vendor set postgres`, CWD); + + await execAsync( + `${BINARY} config db-dsn set "host=localhost user=postgres password=postgres dbname=fireback_test port=5432 sslmode=disable TimeZone=UTC"`, + CWD, + ); + + await execAsync(`${BINARY} migration apply`, CWD); + + return true; + } catch (err) { + console.error("setup postgres failed:", err); + } } else { await execAsync(`${BINARY} config db-vendor set sqlite`, CWD); await execAsync( `${BINARY} config db-name set /tmp/${dbname}.db`, - CWD + CWD, ); await execAsync(`${BINARY} migration apply`, CWD); diff --git a/go.sum b/go.sum index fef937a47..1b64297ef 100644 --- a/go.sum +++ b/go.sum @@ -111,7 +111,7 @@ cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2b cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987794e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/474420502/gcurl v1.2.1 h1:Z+bAiOWdW7hbvTuBKxzhNuCevvvLlHDvk3hoSwHD2RA= diff --git a/modules/abac/AppMenuEntity.dyno.go b/modules/abac/AppMenuEntity.dyno.go index 588544d21..82a3adf29 100644 --- a/modules/abac/AppMenuEntity.dyno.go +++ b/modules/abac/AppMenuEntity.dyno.go @@ -250,8 +250,8 @@ var AppMenuEntityMetaConfig map[string]int64 = map[string]int64{} var AppMenuEntityJsonSchema = fireback.ExtractEntityFields(reflect.ValueOf(&AppMenuEntity{})) type AppMenuEntityPolyglot struct { - LinkerId string `gorm:"uniqueId;not null;size:100;" json:"linkerId,omitempty" yaml:"linkerId,omitempty" xml:"linkerId,omitempty"` - LanguageId string `gorm:"uniqueId;not null;size:100;" json:"languageId,omitempty" xml:"languageId,omitempty" yaml:"languageId,omitempty"` + LinkerId string `gorm:"not null;index:idx_linker_language_appMenu_AppMenuEntityPolyglot,unique" json:"linkerId,omitempty" yaml:"linkerId,omitempty" xml:"linkerId,omitempty"` + LanguageId string `gorm:"not null;index:idx_linker_language_appMenu_AppMenuEntityPolyglot,unique" json:"languageId,omitempty" xml:"languageId,omitempty" yaml:"languageId,omitempty"` Label string `yaml:"label,omitempty" xml:"label,omitempty" json:"label,omitempty"` } @@ -461,7 +461,36 @@ func AppMenuActionCreateFn(dto *AppMenuEntity, query fireback.QueryDSL) (*AppMen dbref = query.Tx } query.Tx = dbref - err := dbref.Create(&dto).Error + err := dbref.Transaction(func(tx *gorm.DB) error { + query.Tx = tx + if err := tx. + Omit("Translations"). + Create(&dto).Error; err != nil { + return err + } + // create translations + if len(dto.Translations) > 0 { + for _, tr := range dto.Translations { + tr.LinkerId = dto.UniqueId + } + if tx.Dialector.Name() == "postgres" { + tx = tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{ + {Name: "linker_id"}, + {Name: "language_id"}, + }, + DoUpdates: clause.AssignmentColumns([]string{ + "label", + }), + }) + } + if err := tx. + Create(&dto.Translations).Error; err != nil { + return err + } + } + return nil + }) if err != nil { err := fireback.GormErrorToIError(err) return nil, err diff --git a/modules/abac/TimezoneGroupEntity.dyno.go b/modules/abac/TimezoneGroupEntity.dyno.go index 3c79efa38..782adc46e 100644 --- a/modules/abac/TimezoneGroupEntity.dyno.go +++ b/modules/abac/TimezoneGroupEntity.dyno.go @@ -213,8 +213,8 @@ var TimezoneGroupEntityMetaConfig map[string]int64 = map[string]int64{} var TimezoneGroupEntityJsonSchema = fireback.ExtractEntityFields(reflect.ValueOf(&TimezoneGroupEntity{})) type TimezoneGroupEntityPolyglot struct { - LinkerId string `gorm:"uniqueId;not null;size:100;" json:"linkerId,omitempty" yaml:"linkerId,omitempty" xml:"linkerId,omitempty"` - LanguageId string `gorm:"uniqueId;not null;size:100;" json:"languageId,omitempty" xml:"languageId,omitempty" yaml:"languageId,omitempty"` + LinkerId string `gorm:"not null;index:idx_linker_language_timezoneGroup_TimezoneGroupEntityPolyglot,unique" json:"linkerId,omitempty" yaml:"linkerId,omitempty" xml:"linkerId,omitempty"` + LanguageId string `gorm:"not null;index:idx_linker_language_timezoneGroup_TimezoneGroupEntityPolyglot,unique" json:"languageId,omitempty" xml:"languageId,omitempty" yaml:"languageId,omitempty"` Title string `yaml:"title,omitempty" xml:"title,omitempty" json:"title,omitempty"` } @@ -420,7 +420,36 @@ func TimezoneGroupActionCreateFn(dto *TimezoneGroupEntity, query fireback.QueryD dbref = query.Tx } query.Tx = dbref - err := dbref.Create(&dto).Error + err := dbref.Transaction(func(tx *gorm.DB) error { + query.Tx = tx + if err := tx. + Omit("Translations"). + Create(&dto).Error; err != nil { + return err + } + // create translations + if len(dto.Translations) > 0 { + for _, tr := range dto.Translations { + tr.LinkerId = dto.UniqueId + } + if tx.Dialector.Name() == "postgres" { + tx = tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{ + {Name: "linker_id"}, + {Name: "language_id"}, + }, + DoUpdates: clause.AssignmentColumns([]string{ + "title", + }), + }) + } + if err := tx. + Create(&dto.Translations).Error; err != nil { + return err + } + } + return nil + }) if err != nil { err := fireback.GormErrorToIError(err) return nil, err diff --git a/modules/abac/WorkspaceTypeEntity.dyno.go b/modules/abac/WorkspaceTypeEntity.dyno.go index cceeeb66e..f1a3a5e32 100644 --- a/modules/abac/WorkspaceTypeEntity.dyno.go +++ b/modules/abac/WorkspaceTypeEntity.dyno.go @@ -235,8 +235,8 @@ var WorkspaceTypeEntityMetaConfig map[string]int64 = map[string]int64{} var WorkspaceTypeEntityJsonSchema = fireback.ExtractEntityFields(reflect.ValueOf(&WorkspaceTypeEntity{})) type WorkspaceTypeEntityPolyglot struct { - LinkerId string `gorm:"uniqueId;not null;size:100;" json:"linkerId,omitempty" yaml:"linkerId,omitempty" xml:"linkerId,omitempty"` - LanguageId string `gorm:"uniqueId;not null;size:100;" json:"languageId,omitempty" xml:"languageId,omitempty" yaml:"languageId,omitempty"` + LinkerId string `gorm:"not null;index:idx_linker_language_workspaceType_WorkspaceTypeEntityPolyglot,unique" json:"linkerId,omitempty" yaml:"linkerId,omitempty" xml:"linkerId,omitempty"` + LanguageId string `gorm:"not null;index:idx_linker_language_workspaceType_WorkspaceTypeEntityPolyglot,unique" json:"languageId,omitempty" xml:"languageId,omitempty" yaml:"languageId,omitempty"` Title string `yaml:"title,omitempty" xml:"title,omitempty" json:"title,omitempty"` Description string `yaml:"description,omitempty" xml:"description,omitempty" json:"description,omitempty"` } @@ -456,7 +456,37 @@ func WorkspaceTypeActionCreateFn(dto *WorkspaceTypeEntity, query fireback.QueryD dbref = query.Tx } query.Tx = dbref - err := dbref.Create(&dto).Error + err := dbref.Transaction(func(tx *gorm.DB) error { + query.Tx = tx + if err := tx. + Omit("Translations"). + Create(&dto).Error; err != nil { + return err + } + // create translations + if len(dto.Translations) > 0 { + for _, tr := range dto.Translations { + tr.LinkerId = dto.UniqueId + } + if tx.Dialector.Name() == "postgres" { + tx = tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{ + {Name: "linker_id"}, + {Name: "language_id"}, + }, + DoUpdates: clause.AssignmentColumns([]string{ + "title", + "description", + }), + }) + } + if err := tx. + Create(&dto.Translations).Error; err != nil { + return err + } + } + return nil + }) if err != nil { err := fireback.GormErrorToIError(err) return nil, err diff --git a/modules/abac/queries/AppMenuCte.vsql b/modules/abac/queries/AppMenuCte.vsql index 4b87c48ad..43bfa9953 100644 --- a/modules/abac/queries/AppMenuCte.vsql +++ b/modules/abac/queries/AppMenuCte.vsql @@ -34,7 +34,7 @@ and app_menu_entity_polyglots.language_id = '(language)' {{ end }} {{ end }} -{{ if .IsMysql }} +{{ if or .IsMysql .IsPostgres }} {{ if .IsCounter }} select count(*) total_items diff --git a/modules/abac/queries/WorkspaceCte.vsql b/modules/abac/queries/WorkspaceCte.vsql index 243c74b8d..2101b633e 100644 --- a/modules/abac/queries/WorkspaceCte.vsql +++ b/modules/abac/queries/WorkspaceCte.vsql @@ -30,7 +30,7 @@ FROM workspace_entities_cte {{ end }} {{ end }} -{{ if .IsMysql }} +{{ if or .IsMysql .IsPostgres }} {{ if .IsCounter }} select count(*) total_items diff --git a/modules/fireback/CapabilityEntity.dyno.go b/modules/fireback/CapabilityEntity.dyno.go index 946015138..3a403c184 100644 --- a/modules/fireback/CapabilityEntity.dyno.go +++ b/modules/fireback/CapabilityEntity.dyno.go @@ -218,8 +218,8 @@ var CapabilityEntityMetaConfig map[string]int64 = map[string]int64{} var CapabilityEntityJsonSchema = ExtractEntityFields(reflect.ValueOf(&CapabilityEntity{})) type CapabilityEntityPolyglot struct { - LinkerId string `gorm:"uniqueId;not null;size:100;" json:"linkerId,omitempty" yaml:"linkerId,omitempty" xml:"linkerId,omitempty"` - LanguageId string `gorm:"uniqueId;not null;size:100;" json:"languageId,omitempty" xml:"languageId,omitempty" yaml:"languageId,omitempty"` + LinkerId string `gorm:"not null;index:idx_linker_language_capability_CapabilityEntityPolyglot,unique" json:"linkerId,omitempty" yaml:"linkerId,omitempty" xml:"linkerId,omitempty"` + LanguageId string `gorm:"not null;index:idx_linker_language_capability_CapabilityEntityPolyglot,unique" json:"languageId,omitempty" xml:"languageId,omitempty" yaml:"languageId,omitempty"` Description string `yaml:"description,omitempty" xml:"description,omitempty" json:"description,omitempty"` } @@ -426,7 +426,36 @@ func CapabilityActionCreateFn(dto *CapabilityEntity, query QueryDSL) (*Capabilit dbref = query.Tx } query.Tx = dbref - err := dbref.Create(&dto).Error + err := dbref.Transaction(func(tx *gorm.DB) error { + query.Tx = tx + if err := tx. + Omit("Translations"). + Create(&dto).Error; err != nil { + return err + } + // create translations + if len(dto.Translations) > 0 { + for _, tr := range dto.Translations { + tr.LinkerId = dto.UniqueId + } + if tx.Dialector.Name() == "postgres" { + tx = tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{ + {Name: "linker_id"}, + {Name: "language_id"}, + }, + DoUpdates: clause.AssignmentColumns([]string{ + "description", + }), + }) + } + if err := tx. + Create(&dto.Translations).Error; err != nil { + return err + } + } + return nil + }) if err != nil { err := GormErrorToIError(err) return nil, err diff --git a/modules/fireback/CoreUtils.go b/modules/fireback/CoreUtils.go index 90d35b2f5..54f0c08df 100644 --- a/modules/fireback/CoreUtils.go +++ b/modules/fireback/CoreUtils.go @@ -497,9 +497,7 @@ func askProjectDatabase(projectName string) (Database, error) { DATABASE_TYPE_SQLITE_MEMORY, DATABASE_TYPE_MYSQL, DATABASE_TYPE_MARIADB, - // Postgres is not well tested yet, we are not adding production ready - // features anymore in fireback at all. - // DATABASE_TYPE_POSTGRES, + DATABASE_TYPE_POSTGRES, }, } diff --git a/modules/fireback/CrudCoreActions.go b/modules/fireback/CrudCoreActions.go index 6c6873c27..65a78688b 100644 --- a/modules/fireback/CrudCoreActions.go +++ b/modules/fireback/CrudCoreActions.go @@ -266,9 +266,10 @@ func UnsafeQuerySqlFromFs[T any](fsRef *embed.FS, queryName string, query QueryD } type VSqlContext struct { - IsMysql bool - IsSqlite bool - IsCounter bool + IsMysql bool + IsSqlite bool + IsPostgres bool + IsCounter bool } func ContextAwareVSqlOperation[T any](refl reflect.Value, fsRef *embed.FS, queryName string, query QueryDSL, values ...interface{}) ([]*T, *QueryResultMeta, *IError) { @@ -309,6 +310,10 @@ func ContextAwareVSqlOperation[T any](refl reflect.Value, fsRef *embed.FS, query ctx.IsSqlite = true } + if vendor == "postgres" { + ctx.IsPostgres = true + } + var output bytes.Buffer tmpl, err := template.New("example").Parse(content) if err != nil { @@ -338,6 +343,11 @@ func ContextAwareVSqlOperation[T any](refl reflect.Value, fsRef *embed.FS, query ctx.IsSqlite = true } + if vendor == "postgres" { + + ctx.IsPostgres = true + } + var output bytes.Buffer tmpl, err := template.New("example").Parse(content) if err != nil { diff --git a/modules/fireback/XDateTimeType.go b/modules/fireback/XDateTimeType.go index 7c571e6d3..a3bd6ac3b 100644 --- a/modules/fireback/XDateTimeType.go +++ b/modules/fireback/XDateTimeType.go @@ -13,6 +13,7 @@ import ( ptime "github.com/yaa110/go-persian-calendar" "gorm.io/gorm" "gorm.io/gorm/clause" + "gorm.io/gorm/schema" ) type XDateTime string @@ -158,13 +159,24 @@ func (date XDateTime) Value() (driver.Value, error) { if date == "" { return nil, nil } - return string(date), nil + t, err := time.Parse(time.RFC3339, string(date)) + if err != nil { + return nil, err + } + + return t, nil } -// GormDataType gorm common data type -func (date XDateTime) GormDataType() string { - return "datetime" +func (date XDateTime) GormDBDataType(db *gorm.DB, field *schema.Field) string { + switch db.Dialector.Name() { + case "postgres": + return "timestamptz" + case "mysql": + return "datetime" + default: + return "datetime" + } } func (js XDateTime) GormValue(ctx context.Context, db *gorm.DB) clause.Expr { @@ -173,6 +185,12 @@ func (js XDateTime) GormValue(ctx context.Context, db *gorm.DB) clause.Expr { } switch db.Dialector.Name() { + case "postgres": + t, err := time.Parse(time.RFC3339, string(js)) + if err == nil { + return gorm.Expr("?", t) + } + case "mysql": parsedTime, err := time.Parse(time.RFC3339, string(js)) if err == nil { diff --git a/modules/fireback/codegen/firebackgo/SharedSnippets.tpl b/modules/fireback/codegen/firebackgo/SharedSnippets.tpl index eaae6e930..6657aa5f3 100644 --- a/modules/fireback/codegen/firebackgo/SharedSnippets.tpl +++ b/modules/fireback/codegen/firebackgo/SharedSnippets.tpl @@ -183,8 +183,10 @@ import "{{ $key}}" {{ if .e.HasTranslations }} type {{ .e.PolyglotName}} struct { - LinkerId string `gorm:"uniqueId;not null;size:100;" json:"linkerId,omitempty" yaml:"linkerId,omitempty" xml:"linkerId,omitempty"` - LanguageId string `gorm:"uniqueId;not null;size:100;" json:"languageId,omitempty" xml:"languageId,omitempty" yaml:"languageId,omitempty"` + LinkerId string `gorm:"not null;index:idx_linker_language_{{.e.Name}}_{{.e.PolyglotName}},unique" json:"linkerId,omitempty" yaml:"linkerId,omitempty" xml:"linkerId,omitempty"` + LanguageId string `gorm:"not null;index:idx_linker_language_{{.e.Name}}_{{.e.PolyglotName}},unique" json:"languageId,omitempty" xml:"languageId,omitempty" yaml:"languageId,omitempty"` + + {{ range .e.CompleteFields }} {{ if .Translate }} @@ -688,12 +690,58 @@ func {{ .e.Upper }}ActionCreateFn(dto *{{ .e.EntityName }}, query {{ .wsprefix } } query.Tx = dbref; - err := dbref.Create(&dto).Error - if err != nil { + + + {{ if .e.HasTranslations }} + err := dbref.Transaction(func(tx *gorm.DB) error { + query.Tx = tx + + if err := tx. + Omit("Translations"). + Create(&dto).Error; err != nil { + return err + } + + // create translations + if len(dto.Translations) > 0 { + + for _, tr := range dto.Translations { + tr.LinkerId = dto.UniqueId + } + + if tx.Dialector.Name() == "postgres" { + tx = tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{ + {Name: "linker_id"}, + {Name: "language_id"}, + }, + DoUpdates: clause.AssignmentColumns([]string{ + {{ range .e.CompleteFields }} + {{ if .Translate }} + "{{.Name}}", + {{ end }} + {{ end }} + }), + }) + } + + if err := tx. + Create(&dto.Translations).Error; err != nil { + return err + } + } + + return nil + }) + + {{ else }} + err := dbref.Create(&dto).Error + {{ end }} + if err != nil { err := {{ .wsprefix }}GormErrorToIError(err) return nil, err } - + // 5. Create sub entities, objects or arrays, association to other entities {{ .e.Upper }}AssociationCreate(dto, query) diff --git a/modules/fireback/codegen/firebackgo/SqlCteQuery.tpl b/modules/fireback/codegen/firebackgo/SqlCteQuery.tpl index a9b82d8e7..96fda6d23 100644 --- a/modules/fireback/codegen/firebackgo/SqlCteQuery.tpl +++ b/modules/fireback/codegen/firebackgo/SqlCteQuery.tpl @@ -40,9 +40,7 @@ {{"{{"}} end {{"}}"}} {{"{{"}} end {{"}}"}} - - -{{"{{"}} if .IsMysql {{"}}"}} +{{"{{"}} if or .IsMysql .IsPostgres {{"}}"}} {{"{{"}} if .IsCounter {{"}}"}} select count(*) total_items