API features/fixes

* Enabled use of PK attributes as lookups.
* Improved API call handling.
* API Builder improvements.
This commit is contained in:
Gabriel Herbert
2023-03-02 23:06:00 +01:00
parent b7955257b8
commit 3bb30a4f02
24 changed files with 635 additions and 291 deletions
+5 -1
View File
@@ -17,6 +17,7 @@ import (
"github.com/gofrs/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
var regexRelId = regexp.MustCompile(`^\_r(\d+)id`) // finds: _r3id
@@ -736,7 +737,10 @@ func addWhere(filter types.DataGetFilter, queryArgs *[]interface{},
}
if isLikeOperator(filter.Operator) {
// special syntax for ILIKE comparison (add wildcard characters)
if v, ok := s.Value.(pgtype.Text); ok {
s.Value = v.String
}
// special syntax for (I)LIKE comparison (add wildcard characters)
s.Value = fmt.Sprintf("%%%s%%", s.Value)
}
+295
View File
@@ -0,0 +1,295 @@
package data_import
import (
"context"
"errors"
"fmt"
"r3/cache"
"r3/data"
"r3/handler"
"r3/schema"
"r3/tools"
"r3/types"
"strings"
"github.com/gofrs/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
)
func resolveQueryLookups(joins []types.QueryJoin, lookups []types.QueryLookup) map[int][]uuid.UUID {
indexMapPgIndexAttributeIds := make(map[int][]uuid.UUID)
for _, join := range joins {
for _, lookup := range lookups {
if lookup.Index != join.Index {
continue
}
rel, exists := cache.RelationIdMap[join.RelationId]
if !exists {
continue
}
for _, pgi := range rel.Indexes {
if lookup.PgIndexId != pgi.Id {
continue
}
atrIds := make([]uuid.UUID, 0)
for _, atr := range pgi.Attributes {
atrIds = append(atrIds, atr.AttributeId)
}
indexMapPgIndexAttributeIds[join.Index] = atrIds
}
}
}
return indexMapPgIndexAttributeIds
}
func FromInterfaceValues_tx(ctx context.Context, tx pgx.Tx, loginId int64,
valuesIn []interface{}, columns []types.Column, joins []types.QueryJoin,
lookups []types.QueryLookup) error {
if len(valuesIn) != len(columns) {
return errors.New("column and value count do not match")
}
// prepare data SET structure and build join index map for reference
dataSetsByIndex := make(map[int]types.DataSet)
joinsByIndex := make(map[int]types.QueryJoin)
for _, join := range joins {
dataSetsByIndex[join.Index] = types.DataSet{
RelationId: join.RelationId,
AttributeId: join.AttributeId.Bytes,
IndexFrom: join.IndexFrom,
RecordId: 0,
Attributes: make([]types.DataSetAttribute, 0),
}
joinsByIndex[join.Index] = join
}
// parse all column values
for i, column := range columns {
atr, exists := cache.AttributeIdMap[column.AttributeId]
if !exists {
return handler.ErrSchemaUnknownAttribute(column.AttributeId)
}
if atr.Encrypted {
return errors.New("cannot handle value for encrypted attribute")
}
dataSet := dataSetsByIndex[column.Index]
dataSet.Attributes = append(dataSet.Attributes, types.DataSetAttribute{
AttributeId: column.AttributeId,
AttributeIdNm: pgtype.UUID{},
OutsideIn: false,
Value: valuesIn[i],
})
dataSetsByIndex[column.Index] = dataSet
}
// lookup record IDs for dataSets relations via defined, unique PG indexes
// a unique PG index consists of 1+ attributes, identifying a single record
// if record IDs can not be identified, new records are created
// by collecting parsed values from the CSV input we can lookup records
// unless PG index includes a relationship attribute, then we can only hope that
// the referenced record is also looked up successfully via a different, unique PG index
indexesResolved := make([]int, 0)
indexMapPgIndexAttributeIds := resolveQueryLookups(joins, lookups)
// multiple attempts can be necessary as PG indexes can use relationship attributes
// these attribute values, if they are available at all, need to be resolved as well
// example: Relation 'department', unique PG index: 'department.company + department.name'
// this PG index allows for unique department names inside companies (but same names across companies)
// in order to resolve this, 'company' must be joined to 'department' in query
// (possibly looked up via unique PG index 'company.name')
// run for number of joins+1 in case all indexes rely on each other in reverse order
attempts := len(joins) + 1
for i := 0; i < attempts; i++ {
for _, join := range joins {
dataSet, _ := dataSetsByIndex[join.Index]
if dataSet.RecordId != 0 {
continue // record already looked up
}
pgIndexAtrIds, exists := indexMapPgIndexAttributeIds[join.Index]
if !exists {
continue // no unique PG index defined, nothing to do
}
if tools.IntInSlice(join.Index, indexesResolved) {
continue // lookup already done
}
names := make([]string, 0)
paras := make([]interface{}, 0)
for _, pgIndexAtrId := range pgIndexAtrIds {
pgIndexAtr, _ := cache.AttributeIdMap[pgIndexAtrId]
if !schema.IsContentRelationship(pgIndexAtr.Content) {
// PG index attribute is non-relationship, can directly be used
for _, setAtr := range dataSet.Attributes {
if setAtr.AttributeId == pgIndexAtr.Id {
names = append(names, pgIndexAtr.Name)
paras = append(paras, setAtr.Value)
break
}
}
} else {
// PG index attribute is a relationship
// check whether this attribute is used to join to/from the required record
for _, ojoin := range joins {
if ojoin.RelationId == pgIndexAtr.RelationshipId.Bytes &&
(ojoin.Index == join.IndexFrom || ojoin.IndexFrom == join.Index) {
oDataSet, exists := dataSetsByIndex[ojoin.Index]
if !exists {
break
}
if oDataSet.RecordId == 0 {
// joined relation found but no record ID was resolved so far
break
}
names = append(names, pgIndexAtr.Name)
paras = append(paras, oDataSet.RecordId)
break
}
}
}
}
if len(names) != len(pgIndexAtrIds) {
// could not resolve all PG index attributes
// attempt is repeated on next loop
continue
}
// execute lookup as values for all PG index attributes were found
rel, exists := cache.RelationIdMap[join.RelationId]
if !exists {
return handler.ErrSchemaUnknownAttribute(join.RelationId)
}
mod := cache.ModuleIdMap[rel.ModuleId]
namesWhere := make([]string, 0)
for i, name := range names {
namesWhere = append(namesWhere, fmt.Sprintf("%s = $%d", name, (i+1)))
}
var recordId int64
err := tx.QueryRow(ctx, fmt.Sprintf(`
SELECT id
FROM %s.%s
WHERE %s
`, mod.Name, rel.Name, strings.Join(namesWhere, "\nAND ")), paras...).Scan(&recordId)
if err == pgx.ErrNoRows {
indexesResolved = append(indexesResolved, join.Index)
continue
}
if err != nil {
return err
}
dataSet.RecordId = recordId
dataSetsByIndex[join.Index] = dataSet
indexesResolved = append(indexesResolved, join.Index)
}
if len(indexesResolved) == len(indexMapPgIndexAttributeIds) {
break
}
}
// apply join create/update restrictions after resolving unique indexes
for _, join := range joins {
if !join.ApplyUpdate && dataSetsByIndex[join.Index].RecordId != 0 {
// existing record but must not update
// remove attribute values - still keep record itself for updating relationship attributes where allowed
dataSet := dataSetsByIndex[join.Index]
dataSet.Attributes = make([]types.DataSetAttribute, 0)
dataSetsByIndex[join.Index] = dataSet
continue
}
if !join.ApplyCreate && dataSetsByIndex[join.Index].RecordId == 0 {
// new record but must not create
// remove entire data SET - if it does not exist and must not be created, it cannot be used as relationship either
delete(dataSetsByIndex, join.Index)
continue
}
}
// update relationship attribute values that point to looked up records
// e. g. if a record was identified, relationship attribute values (if used for join) can be updated
// because relationship attributes cannot be imported directly, resolved records must be added this way
for index, dataSet := range dataSetsByIndex {
if dataSet.RecordId == 0 || dataSet.AttributeId == uuid.Nil {
continue
}
joinAtr, exists := cache.AttributeIdMap[dataSet.AttributeId]
if !exists {
return handler.ErrSchemaUnknownAttribute(dataSet.AttributeId)
}
if joinAtr.RelationId == dataSet.RelationId {
if !joinsByIndex[index].ApplyUpdate {
// only if allowed for this join
continue
}
// join is from this relation (self reference), update attribute for this record
dataSet.Attributes = append(dataSet.Attributes, types.DataSetAttribute{
AttributeId: joinAtr.Id,
AttributeIdNm: pgtype.UUID{},
OutsideIn: false,
Value: dataSet.RecordId,
})
dataSetsByIndex[index] = dataSet
} else {
// join from other relation, update attribute for other record if available
for otherIndex, otherDataSet := range dataSetsByIndex {
if !joinsByIndex[otherIndex].ApplyUpdate {
// only if allowed for this join
continue
}
if otherDataSet.RecordId == 0 {
// join attributes are only relevant for existing records
// new ones get them automatically
continue
}
if joinAtr.RelationId != otherDataSet.RelationId ||
(otherIndex != dataSet.IndexFrom && otherDataSet.IndexFrom != index) {
continue
}
otherDataSet.Attributes = append(otherDataSet.Attributes, types.DataSetAttribute{
AttributeId: joinAtr.Id,
AttributeIdNm: pgtype.UUID{},
OutsideIn: false,
Value: dataSet.RecordId,
})
dataSetsByIndex[otherIndex] = otherDataSet
break
}
}
}
_, err := data.Set_tx(ctx, tx, dataSetsByIndex, loginId)
return err
}
+21 -2
View File
@@ -108,6 +108,23 @@ var upgradeFunctions = map[string]func(tx pgx.Tx) (string, error){
ALTER TABLE app.caption ALTER COLUMN content
TYPE app.caption_content USING content::text::app.caption_content;
-- add references for PKs as PG indexes (used for API)
ALTER TABLE app.pg_index ADD COLUMN primary_key bool NOT NULL DEFAULT false;
ALTER TABLE app.pg_index ALTER COLUMN primary_key DROP DEFAULT;
INSERT INTO app.pg_index (id, relation_id, auto_fki, no_duplicates, primary_key)
SELECT gen_random_uuid(), id, false, true, true FROM app.relation;
INSERT INTO app.pg_index_attribute (pg_index_id, position, order_asc, attribute_id)
SELECT id, 0, true, (
SELECT id
FROM app.attribute
WHERE name = $1
AND relation_id = pgi.relation_id
)
FROM app.pg_index AS pgi
WHERE primary_key;
-- new user setting
ALTER TABLE instance.login_setting ADD COLUMN tab_remember BOOLEAN NOT NULL DEFAULT TRUE;
@@ -302,7 +319,7 @@ var upgradeFunctions = map[string]func(tx pgx.Tx) (string, error){
CASE WHEN field_id IS NULL THEN 0 ELSE 1
END
));
`)
`, schema.PkName)
return "3.3", err
},
"3.1": func(tx pgx.Tx) (string, error) {
@@ -3373,7 +3390,9 @@ var upgradeFunctions = map[string]func(tx pgx.Tx) (string, error){
`, ar.moduleName, ar.attributeId.String())); err != nil {
return "", err
}
if err := pgIndex.SetAutoFkiForAttribute_tx(tx, ar.relationId, ar.attributeId, (ar.content == "1:1")); err != nil {
if err := pgIndex.SetAutoFkiForAttribute_tx(tx, ar.relationId,
ar.attributeId, (ar.content == "1:1")); err != nil {
return "", err
}
}
+136 -103
View File
@@ -10,6 +10,7 @@ import (
"r3/cache"
"r3/config"
"r3/data"
"r3/data/data_import"
"r3/data/data_query"
"r3/db"
"r3/handler"
@@ -23,8 +24,6 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
var handlerContext = "api"
func Handler(w http.ResponseWriter, r *http.Request) {
if blocked := bruteforce.Check(r); blocked {
@@ -36,14 +35,20 @@ func Handler(w http.ResponseWriter, r *http.Request) {
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
var abort = func(httpCode int, errToLog error, errMsgUser string) {
// if not other error is prepared for log, use user error
if errToLog == nil {
errToLog = errors.New(errMsgUser)
}
handler.AbortRequestWithCode(w, "api", httpCode, errToLog, errMsgUser)
}
// check token
var loginId int64
var admin bool
var noAuth bool
if _, err := login_auth.Token(token, &loginId, &admin, &noAuth); err != nil {
handler.AbortRequestWithCode(w, handlerContext, http.StatusUnauthorized,
err, handler.ErrUnauthorized)
abort(http.StatusUnauthorized, err, handler.ErrUnauthorized)
bruteforce.BadAttempt(r)
return
}
@@ -57,26 +62,20 @@ func Handler(w http.ResponseWriter, r *http.Request) {
FROM instance.login_setting
WHERE login_id = $1
`, loginId).Scan(&languageCode); err != nil {
handler.AbortRequestWithCode(w, handlerContext,
http.StatusServiceUnavailable, err, handler.ErrGeneral)
abort(http.StatusServiceUnavailable, err, handler.ErrGeneral)
return
}
var isDelete, isGet, isPatch, isPost bool
var isDelete, isGet, isPost bool
switch r.Method {
case "DELETE":
isDelete = true
case "GET":
isGet = true
case "PATCH":
isPatch = true
case "POST":
isPost = true
default:
handler.AbortRequestWithCode(w, handlerContext, http.StatusBadRequest,
errors.New("invalid HTTP method"), "invalid HTTP method")
abort(http.StatusBadRequest, nil, "invalid HTTP method")
return
}
@@ -88,8 +87,8 @@ func Handler(w http.ResponseWriter, r *http.Request) {
Rules:
Path must contain 5-6 elements (see examples above, split by '/')
6th element is the record ID, required by all except GET/POST
GET/POST can also have record ID (GET: single record lookup, POST: create fixed ID record)
6th element is the record ID, required by all except GET
GET can also have record ID (single record lookup)
*/
elements := strings.Split(r.URL.Path, "/")
recordIdProvided := len(elements) == 6
@@ -100,11 +99,7 @@ func Handler(w http.ResponseWriter, r *http.Request) {
if isGet || isPost {
examplePostfix = " (record ID is optional)"
}
handler.AbortRequestWithCode(w, handlerContext, http.StatusBadRequest,
errors.New("invalid URL"),
fmt.Sprintf("invalid URL, expected: /api/{APP_NAME}/{API_NAME}/{VERSION}/{RECORD_ID}%s", examplePostfix))
abort(http.StatusBadRequest, nil, fmt.Sprintf("invalid URL, expected: /api/{APP_NAME}/{API_NAME}/{VERSION}/{RECORD_ID}%s", examplePostfix))
return
}
@@ -114,9 +109,7 @@ func Handler(w http.ResponseWriter, r *http.Request) {
apiName := elements[3]
version, err := strconv.ParseInt(elements[4][1:], 10, 64) // expected format: "v3"
if err != nil {
handler.AbortRequestWithCode(w, handlerContext, http.StatusBadRequest,
err, fmt.Sprintf("invalid API version format '%s', expected: 'v12'", elements[4]))
abort(http.StatusBadRequest, err, fmt.Sprintf("invalid API version format '%s', expected: 'v12'", elements[4]))
return
}
@@ -124,16 +117,14 @@ func Handler(w http.ResponseWriter, r *http.Request) {
if len(elements) == 6 {
recordId, err = strconv.ParseInt(elements[5], 10, 64)
if err != nil {
handler.AbortRequestWithCode(w, handlerContext, http.StatusBadRequest,
err, fmt.Sprintf("invalid API record ID '%s', integer expected", elements[5]))
abort(http.StatusBadRequest, err, fmt.Sprintf("invalid API record ID '%s', integer expected", elements[5]))
return
}
}
// URL processing complete, actually use API
log.Info("api", fmt.Sprintf("%s.%s (v%d) is called (record ID: %d)",
modName, apiName, version, recordId))
log.Info("api", fmt.Sprintf("%s.%s (v%d) is called with %s (record ID: %d)",
modName, apiName, version, r.Method, recordId))
// resolve API by module+API names
cache.Schema_mx.RLock()
@@ -141,37 +132,67 @@ func Handler(w http.ResponseWriter, r *http.Request) {
apiId, exists := cache.ModuleApiNameMapId[modName][apiName]
if !exists {
handler.AbortRequestWithCode(w, handlerContext, http.StatusNotFound,
fmt.Errorf("API not found, '%s'.'%s'", modName, apiName),
fmt.Sprintf("API not found, '%s'.'%s'", modName, apiName))
abort(http.StatusNotFound, nil, fmt.Sprintf("API '%s.%s' not found", modName, apiName))
return
}
api := cache.ApiIdMap[apiId]
verboseGet := api.VerboseGet
// check supported API methods
if (isDelete && !api.HasDelete) ||
(isGet && !api.HasGet) ||
(isPatch && !api.HasPatch) ||
(isPost && !api.HasPost) {
handler.AbortRequestWithCode(w, handlerContext, http.StatusBadRequest,
fmt.Errorf("unsupported HTTP method '%s'", r.Method),
fmt.Sprintf("HTTP method '%s' is not supported by this API", r.Method))
abort(http.StatusBadRequest, nil, fmt.Sprintf("HTTP method '%s' is not supported by this API", r.Method))
return
}
if !api.Query.RelationId.Valid {
handler.AbortRequestWithCode(w, handlerContext, http.StatusServiceUnavailable,
fmt.Errorf("query has no base relation"), "query has no base relation")
abort(http.StatusServiceUnavailable, nil, "query has no base relation")
return
}
// TEMP
// check role access
// parse general getters
var getters struct {
limit int
offset int
verbose bool
}
getters.limit = api.LimitDef
getters.verbose = api.VerboseDef
for getter, value := range r.URL.Query() {
if len(value) == 1 && getter == "limit" || getter == "offset" || getter == "verbose" {
n, err := strconv.Atoi(value[0])
if err != nil {
abort(http.StatusBadRequest, err, fmt.Sprintf("invalid value '%s' for %s", value[0], getter))
return
}
switch getter {
case "limit":
getters.limit = n
case "offset":
getters.offset = n
case "verbose":
getters.verbose = n == 1
}
}
}
// execute request
ctx, ctxCancel := context.WithTimeout(context.Background(),
time.Duration(int64(config.GetUint64("dbTimeoutDataRest")))*time.Second)
defer ctxCancel()
tx, err := db.Pool.Begin(ctx)
if err != nil {
abort(http.StatusServiceUnavailable, err, handler.ErrGeneral)
return
}
defer tx.Rollback(ctx)
if isDelete {
}
@@ -180,7 +201,17 @@ func Handler(w http.ResponseWriter, r *http.Request) {
dataGet := types.DataGet{
RelationId: api.Query.RelationId.Bytes,
IndexSource: 0,
Limit: getters.limit,
Offset: getters.offset,
}
// abort if requested limit exceeds max limit
// better to abort as smaller than requested result count might suggest the absence of more data
if api.LimitMax < dataGet.Limit {
abort(http.StatusBadRequest, nil, fmt.Sprintf("max. result limit is: %d", api.LimitMax))
return
}
for _, join := range api.Query.Joins {
if join.Index == 0 {
continue
@@ -208,40 +239,6 @@ func Handler(w http.ResponseWriter, r *http.Request) {
dataGet.Expressions = append(dataGet.Expressions, expr)
}
// set API default limit
dataGet.Limit = api.LimitDef
// parse getters
for getter, value := range r.URL.Query() {
if len(value) != 1 {
continue
}
if getter == "limit" || getter == "offset" || getter == "verbose" {
n, err := strconv.ParseInt(value[0], 10, 64)
if err != nil {
handler.AbortRequestWithCode(w, handlerContext, http.StatusBadRequest,
err, fmt.Sprintf("invalid value '%s' for %s", value[0], getter))
return
}
switch getter {
case "limit":
dataGet.Limit = int(n)
case "offset":
dataGet.Offset = int(n)
case "verbose":
verboseGet = n == 1
}
}
}
// enforce API max limit
if api.LimitMax < dataGet.Limit {
dataGet.Limit = api.LimitMax
}
// apply query filters
dataGet.Filters = data_query.ConvertQueryToDataFilter(
api.Query.Filters, loginId, languageCode)
@@ -265,32 +262,20 @@ func Handler(w http.ResponseWriter, r *http.Request) {
dataGet.Orders = data_query.ConvertQueryToDataOrders(api.Query.Orders)
// get data
ctx, ctxCancel := context.WithTimeout(context.Background(),
time.Duration(int64(config.GetUint64("dbTimeoutDataRest")))*time.Second)
defer ctxCancel()
tx, err := db.Pool.Begin(ctx)
if err != nil {
handler.AbortRequest(w, handlerContext, err, handler.ErrGeneral)
return
}
defer tx.Rollback(ctx)
var query string
results, _, err := data.Get_tx(ctx, tx, dataGet, loginId, &query)
if err != nil {
handler.AbortRequest(w, handlerContext, err, handler.ErrGeneral)
return
}
if err := tx.Commit(ctx); err != nil {
handler.AbortRequest(w, handlerContext, err, handler.ErrGeneral)
if err.Error() == handler.ErrUnauthorized {
abort(http.StatusUnauthorized, err, handler.ErrUnauthorized)
return
}
abort(http.StatusServiceUnavailable, err, handler.ErrGeneral)
return
}
// parse output
rows := make([]interface{}, 0)
if !verboseGet {
if !getters.verbose {
for _, result := range results {
rows = append(rows, result.Values)
}
@@ -317,22 +302,70 @@ func Handler(w http.ResponseWriter, r *http.Request) {
payloadJson, err := json.Marshal(rows)
if err != nil {
handler.AbortRequest(w, handlerContext, err, handler.ErrGeneral)
abort(http.StatusServiceUnavailable, err, handler.ErrGeneral)
return
}
w.Write(payloadJson)
return
}
if isPatch {
}
if isPost {
values := make([]interface{}, len(api.Columns))
if !getters.verbose {
// non-verbose mode: values are following columns (equal count and order)
// [123,"Fritz","Hans"]
if err := json.NewDecoder(r.Body).Decode(&values); err != nil {
abort(http.StatusBadRequest, err, "invalid JSON object")
return
}
} else {
// verbose mode: relation index -> attribute name -> value
// convert verbose to non-verbose input (to process both inputs the same way)
/*{
"0":{ "attribute0a":123, "attribute0b":"Fritz" },
"1":{ "attribute1a":"Hans" }
}*/
var jsonObj map[string]map[string]interface{}
if err := json.NewDecoder(r.Body).Decode(&jsonObj); err != nil {
abort(http.StatusBadRequest, err, "invalid JSON object")
return
}
// pre-populate values with nil, in case required attribute values are not given
for i, _ := range api.Columns {
values[i] = nil
}
for relIndexStr, attributeNameMapValues := range jsonObj {
relIndex, err := strconv.Atoi(relIndexStr)
if err != nil {
abort(http.StatusBadRequest, nil, fmt.Sprintf("invalid relation index '%s', integer expected", relIndexStr))
return
}
for i, column := range api.Columns {
if column.Index == relIndex {
atr := cache.AttributeIdMap[column.AttributeId]
if value, exists := attributeNameMapValues[atr.Name]; exists {
values[i] = value
}
}
}
}
}
if err := data_import.FromInterfaceValues_tx(ctx, tx, loginId, values,
api.Columns, api.Query.Joins, api.Query.Lookups); err != nil {
abort(http.StatusServiceUnavailable, err, handler.ErrGeneral)
return
}
w.WriteHeader(http.StatusOK)
}
// should never arrive here, one of the above methods must be valid
handler.AbortRequest(w, handlerContext, err, handler.ErrGeneral)
return
// apply changes
if err := tx.Commit(ctx); err != nil {
abort(http.StatusServiceUnavailable, err, handler.ErrGeneral)
return
}
}
+5 -7
View File
@@ -10,11 +10,9 @@ import (
)
func PgIndexDel_tx(tx pgx.Tx, reqJson json.RawMessage) (interface{}, error) {
var req struct {
Id uuid.UUID `json:"id"`
}
if err := json.Unmarshal(reqJson, &req); err != nil {
return nil, err
}
@@ -22,11 +20,9 @@ func PgIndexDel_tx(tx pgx.Tx, reqJson json.RawMessage) (interface{}, error) {
}
func PgIndexGet(reqJson json.RawMessage) (interface{}, error) {
var req struct {
RelationId uuid.UUID `json:"relationId"`
}
if err := json.Unmarshal(reqJson, &req); err != nil {
return nil, err
}
@@ -34,12 +30,14 @@ func PgIndexGet(reqJson json.RawMessage) (interface{}, error) {
}
func PgIndexSet_tx(tx pgx.Tx, reqJson json.RawMessage) (interface{}, error) {
var req types.PgIndex
if err := json.Unmarshal(reqJson, &req); err != nil {
return nil, err
}
return nil, pgIndex.Set_tx(tx, req.RelationId,
req.Id, req.NoDuplicates, false, req.Attributes)
// overwrite values that can only be set on the backend
req.AutoFki = false
req.PrimaryKey = false
return nil, pgIndex.Set_tx(tx, req)
}
+12 -13
View File
@@ -20,8 +20,8 @@ func Get(moduleId uuid.UUID) ([]types.Api, error) {
apis := make([]types.Api, 0)
rows, err := db.Pool.Query(db.Ctx, `
SELECT id, name, has_delete, has_get, has_patch,
has_post, limit_def, limit_max, verbose_get
SELECT id, name, has_delete, has_get, has_post,
limit_def, limit_max, verbose_def
FROM app.api
WHERE module_id = $1
ORDER BY name ASC
@@ -34,8 +34,8 @@ func Get(moduleId uuid.UUID) ([]types.Api, error) {
var a types.Api
a.ModuleId = moduleId
if err := rows.Scan(&a.Id, &a.Name, &a.HasDelete, &a.HasGet, &a.HasPatch,
&a.HasPost, &a.LimitDef, &a.LimitMax, &a.VerboseGet); err != nil {
if err := rows.Scan(&a.Id, &a.Name, &a.HasDelete, &a.HasGet,
&a.HasPost, &a.LimitDef, &a.LimitMax, &a.VerboseDef); err != nil {
return apis, err
}
@@ -68,22 +68,21 @@ func Set_tx(tx pgx.Tx, api types.Api) error {
if known {
if _, err := tx.Exec(db.Ctx, `
UPDATE app.api
SET name = $1, has_delete = $2, has_get = $3, has_patch = $4,
has_post = $5, limit_def = $6, limit_max = $7, verbose_get = $8
WHERE id = $9
`, api.Name, api.HasDelete, api.HasGet, api.HasPatch, api.HasPost,
api.LimitDef, api.LimitMax, api.VerboseGet, api.Id); err != nil {
SET name = $1, has_delete = $2, has_get = $3, has_post = $4,
limit_def = $5, limit_max = $6, verbose_def = $7
WHERE id = $8
`, api.Name, api.HasDelete, api.HasGet, api.HasPost, api.LimitDef,
api.LimitMax, api.VerboseDef, api.Id); err != nil {
return err
}
} else {
if _, err := tx.Exec(db.Ctx, `
INSERT INTO app.api (id, module_id, name, has_delete, has_get,
has_patch, has_post, limit_def, limit_max, verbose_get)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
has_post, limit_def, limit_max, verbose_def)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
`, api.Id, api.ModuleId, api.Name, api.HasDelete, api.HasGet,
api.HasPatch, api.HasPost, api.LimitDef, api.LimitMax,
api.VerboseGet); err != nil {
api.HasPost, api.LimitDef, api.LimitMax, api.VerboseDef); err != nil {
return err
}
+5
View File
@@ -416,6 +416,11 @@ func Set_tx(tx pgx.Tx, relationId uuid.UUID, id uuid.UUID,
return err
}
// create PK PG index reference
if err := pgIndex.SetPrimaryKeyForAttribute_tx(tx, relationId, id); err != nil {
return err
}
// create table for encrypted record keys if relation supports encryption
if relEncryption {
tName := schema.GetEncKeyTableName(relationId)
+66 -50
View File
@@ -53,55 +53,48 @@ func Del_tx(tx pgx.Tx, id uuid.UUID) error {
return err
}
if _, err := tx.Exec(db.Ctx, `
DELETE FROM app.pg_index
WHERE id = $1
`, id); err != nil {
return err
}
return nil
_, err = tx.Exec(db.Ctx, `DELETE FROM app.pg_index WHERE id = $1`, id)
return err
}
func Get(relationId uuid.UUID) ([]types.PgIndex, error) {
indexes := make([]types.PgIndex, 0)
pgIndexes := make([]types.PgIndex, 0)
rows, err := db.Pool.Query(db.Ctx, `
SELECT id, no_duplicates, auto_fki
SELECT id, no_duplicates, auto_fki, primary_key
FROM app.pg_index
WHERE relation_id = $1
-- an order is required for hash comparisson (module changes)
ORDER BY auto_fki DESC, id ASC
`, relationId)
if err != nil {
return indexes, err
return pgIndexes, err
}
for rows.Next() {
var ind types.PgIndex
var pgi types.PgIndex
if err := rows.Scan(&ind.Id, &ind.NoDuplicates, &ind.AutoFki); err != nil {
return indexes, err
if err := rows.Scan(&pgi.Id, &pgi.NoDuplicates, &pgi.AutoFki, &pgi.PrimaryKey); err != nil {
return pgIndexes, err
}
ind.RelationId = relationId
indexes = append(indexes, ind)
pgi.RelationId = relationId
pgIndexes = append(pgIndexes, pgi)
}
rows.Close()
// get index attributes
for i, ind := range indexes {
for i, pgi := range pgIndexes {
ind.Attributes, err = GetAttributes(ind.Id)
pgi.Attributes, err = GetAttributes(pgi.Id)
if err != nil {
return indexes, err
return pgIndexes, err
}
indexes[i] = ind
pgIndexes[i] = pgi
}
return indexes, nil
return pgIndexes, nil
}
func GetAttributes(pgIndexId uuid.UUID) ([]types.PgIndexAttribute, error) {
attributes := make([]types.PgIndexAttribute, 0)
rows, err := db.Pool.Query(db.Ctx, `
@@ -128,23 +121,44 @@ func GetAttributes(pgIndexId uuid.UUID) ([]types.PgIndexAttribute, error) {
}
func SetAutoFkiForAttribute_tx(tx pgx.Tx, relationId uuid.UUID, attributeId uuid.UUID, noDuplicates bool) error {
atrs := []types.PgIndexAttribute{
types.PgIndexAttribute{
AttributeId: attributeId,
Position: 0,
OrderAsc: true,
return Set_tx(tx, types.PgIndex{
Id: uuid.Nil,
RelationId: relationId,
AutoFki: true,
NoDuplicates: noDuplicates,
PrimaryKey: false,
Attributes: []types.PgIndexAttribute{
types.PgIndexAttribute{
AttributeId: attributeId,
Position: 0,
OrderAsc: true,
},
},
}
return Set_tx(tx, relationId, uuid.Nil, noDuplicates, true, atrs)
})
}
func Set_tx(tx pgx.Tx, relationId uuid.UUID, id uuid.UUID, noDuplicates bool,
autoFki bool, attributes []types.PgIndexAttribute) error {
func SetPrimaryKeyForAttribute_tx(tx pgx.Tx, relationId uuid.UUID, attributeId uuid.UUID) error {
return Set_tx(tx, types.PgIndex{
Id: uuid.Nil,
RelationId: relationId,
AutoFki: false,
NoDuplicates: true,
PrimaryKey: true,
Attributes: []types.PgIndexAttribute{
types.PgIndexAttribute{
AttributeId: attributeId,
Position: 0,
OrderAsc: true,
},
},
})
}
func Set_tx(tx pgx.Tx, pgi types.PgIndex) error {
if len(attributes) == 0 {
if len(pgi.Attributes) == 0 {
return errors.New("cannot create index without attributes")
}
known, err := schema.CheckCreateId_tx(tx, &id, "pg_index", "id")
known, err := schema.CheckCreateId_tx(tx, &pgi.Id, "pg_index", "id")
if err != nil {
return err
}
@@ -156,16 +170,16 @@ func Set_tx(tx pgx.Tx, relationId uuid.UUID, id uuid.UUID, noDuplicates bool,
// insert pg index reference
if _, err := tx.Exec(db.Ctx, `
INSERT INTO app.pg_index (id,relation_id,no_duplicates,auto_fki)
VALUES ($1,$2,$3,$4)
`, id, relationId, noDuplicates, autoFki); err != nil {
INSERT INTO app.pg_index (
id, relation_id, no_duplicates, auto_fki, primary_key)
VALUES ($1,$2,$3,$4,$5)
`, pgi.Id, pgi.RelationId, pgi.NoDuplicates, pgi.AutoFki, pgi.PrimaryKey); err != nil {
return err
}
// work out pg index columns
// work out PG index columns
indexCols := make([]string, 0)
for position, atr := range attributes {
for position, atr := range pgi.Attributes {
name, err := schema.GetAttributeNameById_tx(tx, atr.AttributeId)
if err != nil {
@@ -181,31 +195,33 @@ func Set_tx(tx pgx.Tx, relationId uuid.UUID, id uuid.UUID, noDuplicates bool,
// insert index attribute references
if _, err := tx.Exec(db.Ctx, `
INSERT INTO app.pg_index_attribute (
pg_index_id, attribute_id, position, order_asc
)
pg_index_id, attribute_id, position, order_asc)
VALUES ($1,$2,$3,$4)
`, id, atr.AttributeId, position, atr.OrderAsc); err != nil {
`, pgi.Id, atr.AttributeId, position, atr.OrderAsc); err != nil {
return err
}
}
// primary key already has an index
if pgi.PrimaryKey {
return nil
}
// create index in module
moduleName, relationName, err := schema.GetRelationNamesById_tx(tx, relationId)
moduleName, relationName, err := schema.GetRelationNamesById_tx(tx, pgi.RelationId)
if err != nil {
return err
}
options := "INDEX"
if noDuplicates {
if pgi.NoDuplicates {
options = "UNIQUE INDEX"
}
if _, err := tx.Exec(db.Ctx, fmt.Sprintf(`
_, err = tx.Exec(db.Ctx, fmt.Sprintf(`
CREATE %s "%s" ON "%s"."%s" (%s)
`, options, schema.GetPgIndexName(id), moduleName, relationName,
strings.Join(indexCols, ","))); err != nil {
`, options, schema.GetPgIndexName(pgi.Id), moduleName, relationName,
strings.Join(indexCols, ",")))
return err
}
return nil
return err
}
+1 -4
View File
@@ -183,8 +183,5 @@ func Set_tx(tx pgx.Tx, moduleId uuid.UUID, id uuid.UUID, name string,
}
// set policies
if err := setPolicies_tx(tx, id, policies); err != nil {
return err
}
return nil
return setPolicies_tx(tx, id, policies)
}
+1 -6
View File
@@ -358,7 +358,6 @@ func importModule_tx(tx pgx.Tx, mod types.Module, firstRun bool, lastRun bool,
// PG indexes
for _, relation := range mod.Relations {
for _, e := range relation.Indexes {
run, err := importCheckRunAndSave(tx, firstRun, e.Id, idMapSkipped)
if err != nil {
return err
@@ -368,10 +367,7 @@ func importModule_tx(tx pgx.Tx, mod types.Module, firstRun bool, lastRun bool,
}
log.Info("transfer", fmt.Sprintf("set index %s", e.Id))
if err := importCheckResultAndApply(tx, pgIndex.Set_tx(tx,
e.RelationId, e.Id, e.NoDuplicates, e.AutoFki, e.Attributes),
e.Id, idMapSkipped); err != nil {
if err := importCheckResultAndApply(tx, pgIndex.Set_tx(tx, e), e.Id, idMapSkipped); err != nil {
return err
}
}
@@ -379,7 +375,6 @@ func importModule_tx(tx pgx.Tx, mod types.Module, firstRun bool, lastRun bool,
// forms, refer to relations/attributes/collections/JS functions
for _, e := range mod.Forms {
run, err := importCheckRunAndSave(tx, firstRun, e.Id, idMapSkipped)
if err != nil {
return err
+5 -5
View File
@@ -49,11 +49,10 @@ type Api struct {
Columns []Column `json:"columns"`
HasDelete bool `json:"hasDelete"`
HasGet bool `json:"hasGet"`
HasPatch bool `json:"hasPatch"`
HasPost bool `json:"hasPost"`
LimitDef int `json:"limitDef"` // default limit, if nothing else is specified
LimitMax int `json:"limitMax"` // maximum limit that can be requested
VerboseGet bool `json:"verboseGet"` // default output option for GET, add attribute names as keys
VerboseDef bool `json:"verboseDef"` // default input/output option, verbose shows relation indexes and attribute names
}
type Article struct {
Id uuid.UUID `json:"id"`
@@ -477,9 +476,10 @@ type PgTrigger struct {
type PgIndex struct {
Id uuid.UUID `json:"id"`
RelationId uuid.UUID `json:"relationId"`
NoDuplicates bool `json:"noDuplicates"`
AutoFki bool `json:"autoFki"`
Attributes []PgIndexAttribute `json:"attributes"`
NoDuplicates bool `json:"noDuplicates"` // index is unique
AutoFki bool `json:"autoFki"` // index belongs to foreign key attribute (auto-generated)
PrimaryKey bool `json:"primaryKey"` // index belongs to primary key attribute
Attributes []PgIndexAttribute `json:"attributes"` // attributes the index is made of
}
type PgIndexAttribute struct {
PgIndexId uuid.UUID `json:"pgIndexId"`
+2 -2
View File
@@ -164,7 +164,7 @@ let MyBuilder = {
<router-link class="entry clickable"
:to="'/builder/apis/'+module.id"
>
<img src="images/tray.png" />
<img src="images/api.png" />
<span>{{ capApp.navigationApis }}</span>
</router-link>
@@ -196,7 +196,7 @@ let MyBuilder = {
<h1>{{ capApp.navigationCollections }}</h1>
</div>
<div class="line" v-if="navigation === 'apis'">
<img src="images/tray.png" />
<img src="images/api.png" />
<h1>{{ capApp.navigationApis }}</h1>
</div>
<div class="line" v-if="navigation === 'pg-functions'">
+12 -15
View File
@@ -25,7 +25,7 @@ let MyBuilderApi = {
<div class="contentBox grow">
<div class="top">
<div class="area nowrap">
<img class="icon" src="images/tray.png" />
<img class="icon" src="images/api.png" />
<h1 class="title">
{{ capApp.titleOne.replace('{NAME}',name) }}
</h1>
@@ -122,15 +122,17 @@ let MyBuilderApi = {
@set-filters="filters = $event"
@set-fixed-limit="fixedLimit = $event"
@set-joins="joins = $event"
@set-lookups="lookups = $event"
@set-orders="orders = $event"
@set-relation-id="relationId = $event"
:allowChoices="false"
:allowLookups="false"
:allowLookups="true"
:allowOrders="true"
:builderLanguage="builderLanguage"
:filters="filters"
:fixedLimit="fixedLimit"
:joins="joins"
:lookups="lookups"
:moduleId="module.id"
:orders="orders"
:relationId="relationId"
@@ -202,11 +204,6 @@ let MyBuilderApi = {
<td><my-bool v-model="hasPost" /></td>
<td>{{ capApp.hasPostHint }}</td>
</tr>
<tr>
<td>PATCH</td>
<td><my-bool v-model="hasPatch" /></td>
<td>{{ capApp.hasPatchHint }}</td>
</tr>
<tr>
<td>DELETE</td>
<td><my-bool v-model="hasDelete" /></td>
@@ -249,18 +246,18 @@ let MyBuilderApi = {
joins:[],
filters:[],
orders:[],
lookups:[],
fixedLimit:0,
// inputs
columns:[],
hasDelete:false,
hasGet:false,
hasPatch:false,
hasPost:false,
limitDef:100,
limitMax:1000,
name:'',
verboseGet:false,
verboseDef:false,
// state
columnIdShow:null,
@@ -282,16 +279,16 @@ let MyBuilderApi = {
hasChanges:(s) => s.name !== s.api.name
|| s.hasDelete !== s.api.hasDelete
|| s.hasGet !== s.api.hasGet
|| s.hasPatch !== s.api.hasPatch
|| s.hasPost !== s.api.hasPost
|| s.limitDef !== s.api.limitDef
|| s.limitMax !== s.api.limitMax
|| s.verboseGet !== s.api.verboseGet
|| s.verboseDef !== s.api.verboseDef
|| s.relationId !== s.api.query.relationId
|| s.fixedLimit !== s.api.query.fixedLimit
|| JSON.stringify(s.joins) !== JSON.stringify(s.api.query.joins)
|| JSON.stringify(s.filters) !== JSON.stringify(s.api.query.filters)
|| JSON.stringify(s.orders) !== JSON.stringify(s.api.query.orders)
|| JSON.stringify(s.lookups) !== JSON.stringify(s.api.query.lookups)
|| JSON.stringify(s.columns) !== JSON.stringify(s.api.columns),
// simple
@@ -341,16 +338,16 @@ let MyBuilderApi = {
this.name = this.api.name;
this.hasDelete = this.api.hasDelete;
this.hasGet = this.api.hasGet;
this.hasPatch = this.api.hasPatch;
this.hasPost = this.api.hasPost;
this.limitDef = this.api.limitDef;
this.limitMax = this.api.limitMax;
this.verboseGet = this.api.verboseGet;
this.verboseDef = this.api.verboseDef;
this.relationId = this.api.query.relationId;
this.fixedLimit = this.api.query.fixedLimit;
this.joins = JSON.parse(JSON.stringify(this.api.query.joins));
this.filters = JSON.parse(JSON.stringify(this.api.query.filters));
this.orders = JSON.parse(JSON.stringify(this.api.query.orders));
this.lookups = JSON.parse(JSON.stringify(this.api.query.lookups));
this.columns = JSON.parse(JSON.stringify(this.api.columns));
this.columnIdShow = null;
},
@@ -409,15 +406,15 @@ let MyBuilderApi = {
joins:this.joins,
filters:this.filters,
orders:this.orders,
lookups:this.lookups,
fixedLimit:this.fixedLimit
},
hasDelete:this.hasDelete,
hasGet:this.hasGet,
hasPatch:this.hasPatch,
hasPost:this.hasPost,
limitDef:this.limitDef,
limitMax:this.limitMax,
verboseGet:this.verboseGet
verboseDef:this.verboseDef
}),
ws.prepare('schema','check',{moduleId:this.module.id})
],true).then(
+1 -1
View File
@@ -5,7 +5,7 @@ let MyBuilderApis = {
template:`<div class="builder-apis contentBox grow">
<div class="top lower">
<div class="area nowrap">
<img class="icon" src="images/tray.png" />
<img class="icon" src="images/api.png" />
<h1 class="title">{{ capApp.title }}</h1>
</div>
<div class="area default-inputs">
+1 -1
View File
@@ -44,7 +44,7 @@ let MyBuilderAttribute = {
<td>{{ capGen.name }}</td>
<td>
<div class="row gap centered">
<input v-model="values.name" :disabled="readonly || isId" />
<input v-focus v-model="values.name" :disabled="readonly || isId" />
<my-button image="visible1.png"
@trigger="copyValueDialog(values.name,attributeId,attributeId)"
:active="!isNew"
+3 -4
View File
@@ -23,7 +23,7 @@ let MyBuilderNew = {
<div class="content default-inputs">
<div class="row gap centered">
<span>{{ capGen.name }}</span>
<input v-model="name" />
<input v-model="name" v-focus />
</div>
<div
@@ -118,7 +118,7 @@ let MyBuilderNew = {
},
titleImgSrc:(s) => {
switch(s.entity) {
case 'api': return 'images/tray.png'; break;
case 'api': return 'images/api.png'; break;
case 'collection': return 'images/tray.png'; break;
case 'form': return 'images/fileText.png'; break;
case 'jsFunction': return 'images/codeScreen.png'; break;
@@ -173,11 +173,10 @@ let MyBuilderNew = {
query:this.getQueryTemplate(),
hasDelete:false,
hasGet:true,
hasPatch:false,
hasPost:false,
limitDef:100,
limitMax:1000,
verboseGet:true
verboseDef:true
};
break;
case 'collection':
+19 -20
View File
@@ -13,7 +13,7 @@ let MyBuilderPgIndex = {
<my-button image="delete.png"
v-if="!isNew"
@trigger="del"
:active="!autoFki && !readonly"
:active="!autoFki && !primaryKey && !readonly"
:cancel="true"
:captionTitle="capGen.button.delete"
/>
@@ -36,6 +36,7 @@ let MyBuilderPgIndex = {
</template>
</select>
</td>
<td><my-bool v-model="primaryKey" :readonly="true" /></td>
<td><my-bool v-model="autoFki" :readonly="true" /></td>
<td><my-bool v-model="noDuplicates" :readonly="!isNew || readonly" /></td>
</tr>`,
@@ -45,72 +46,70 @@ let MyBuilderPgIndex = {
id:null,
autoFki:false,
noDuplicates:false,
primaryKey:false,
attributes:[]
}}
},
readonly:{ type:Boolean, required:true },
relation:{ type:Object, required:true }
},
data:function() {
data() {
return {
attributeInput:'',
// values
autoFki:this.index.autoFki,
noDuplicates:this.index.noDuplicates,
primaryKey:this.index.primaryKey,
attributes:JSON.parse(JSON.stringify(this.index.attributes))
};
},
computed:{
attributeIdsUsed:function() {
attributeIdsUsed:(s) => {
let ids = [];
for(let i = 0, j = this.attributes.length; i < j; i++) {
ids.push(this.attributes[i].attributeId);
for(let a of s.attributes) {
ids.push(a.attributeId);
}
return ids;
},
pgIndexAttributesCaption:function() {
pgIndexAttributesCaption:(s) => {
let out = [];
for(let i = 0, j = this.attributes.length; i < j; i++) {
let atr = this.attributeIdMap[this.attributes[i].attributeId];
out.push(this.getAttributeCaption(atr.name,this.attributes[i].orderAsc));
for(let a of s.attributes) {
out.push(s.getAttributeCaption(s.attributeIdMap[a.attributeId].name,a.orderAsc));
}
return out.join(' + ');
},
// simple states
isNew:function() { return this.index.id === null; },
isNew:(s) => s.index.id === null,
// stores
attributeIdMap:function() { return this.$store.getters['schema/attributeIdMap']; },
capApp: function() { return this.$store.getters.captions.builder.relation; },
capGen: function() { return this.$store.getters.captions.generic; }
attributeIdMap:(s) => s.$store.getters['schema/attributeIdMap'],
capApp: (s) => s.$store.getters.captions.builder.relation,
capGen: (s) => s.$store.getters.captions.generic
},
methods:{
addAttribute:function() {
addAttribute() {
if(this.attributeInput === '') return;
let s = this.attributeInput.split('_');
this.attributes.push({
attributeId:s[0],
orderAsc:s[1] === 'ASC'
});
this.attributeInput = '';
},
getAttributeCaption:function(attributeName,orderAsc) {
getAttributeCaption(attributeName,orderAsc) {
return `${attributeName} (${orderAsc ? 'ASC' : 'DESC'})`;
},
del:function(rel) {
del(rel) {
ws.send('pgIndex','del',{id:this.index.id},true).then(
() => this.$root.schemaReload(this.relation.moduleId),
this.$root.genericError
);
},
set:function() {
set() {
ws.send('pgIndex','set',{
id:this.index.id,
relationId:this.relation.id,
+32 -45
View File
@@ -78,25 +78,25 @@ let MyBuilderQueryChoice = {
emits:['move-down','move-up','remove','update'],
computed:{
filtersInput:{
get:function() { return JSON.parse(JSON.stringify(this.choice.filters)); },
set:function(v) { this.update('filters',v); }
get() { return JSON.parse(JSON.stringify(this.choice.filters)); },
set(v) { this.update('filters',v); }
},
nameInput:{
get:function() { return this.choice.name; },
set:function(v) { this.update('name',v); }
get() { return this.choice.name; },
set(v) { this.update('name',v); }
},
// stores
capApp:function() { return this.$store.getters.captions.builder.query; },
capGen:function() { return this.$store.getters.captions.generic; }
capApp:(s) => s.$store.getters.captions.builder.query,
capGen:(s) => s.$store.getters.captions.generic
},
methods:{
updateCaption:function(content,value) {
updateCaption(content,value) {
let captionsInput = JSON.parse(JSON.stringify(this.choice.captions));
captionsInput[content] = value;
this.update('captions',captionsInput);
},
update:function(content,value) {
update(content,value) {
let choice = JSON.parse(JSON.stringify(this.choice));
choice[content] = value;
this.$emit('update',choice);
@@ -122,16 +122,13 @@ let MyBuilderQueryLookupItem = {
emits:['update:modelValue'],
computed:{
value:{
get:function() { return this.modelValue; },
set:function(v) { this.$emit('update:modelValue',v); }
get() { return this.modelValue; },
set(v) { this.$emit('update:modelValue',v); }
},
pgIndexCandidates:function() {
pgIndexCandidates:(s) => {
let out = [];
let rel = this.relationIdMap[this.join.relationId];
for(let i = 0, j = rel.indexes.length; i < j; i++) {
let index = rel.indexes[i];
let rel = s.relationIdMap[s.join.relationId];
for(let index of rel.indexes) {
if(index.noDuplicates)
out.push(index);
}
@@ -139,16 +136,14 @@ let MyBuilderQueryLookupItem = {
},
// stores
relationIdMap: function() { return this.$store.getters['schema/relationIdMap']; },
attributeIdMap:function() { return this.$store.getters['schema/attributeIdMap']; }
relationIdMap: (s) => s.$store.getters['schema/relationIdMap'],
attributeIdMap:(s) => s.$store.getters['schema/attributeIdMap']
},
methods:{
displayPgIndexDesc:function(pgIndex) {
displayPgIndexDesc(pgIndex) {
let out = [];
for(let i = 0, j = pgIndex.attributes.length; i < j; i++) {
let atr = this.attributeIdMap[pgIndex.attributes[i].attributeId];
for(let a of pgIndex.attributes) {
let atr = this.attributeIdMap[a.attributeId];
out.push(`${atr.name} (${atr.content})`);
}
return out.join(' + ');
@@ -171,45 +166,37 @@ let MyBuilderQueryLookups = {
</div>`,
props:{
joins: { type:Array, required:true },
lookups:{ type:Array, required:true }
lookups:{ type:Array, required:true } // [{pgIndexId:123,index:0},{...}]
},
emits:['update'],
computed:{
lookupsInput:{
get:function() { return JSON.parse(JSON.stringify(this.lookups)); },
set:function(v) { this.$emit('update',v); }
},
// stores
capApp:function() { return this.$store.getters.captions.builder.query; }
capApp:(s) => s.$store.getters.captions.builder.query
},
methods:{
getValueForJoin:function(join) {
for(let i = 0, j = this.lookupsInput.length; i < j; i++) {
if(this.lookupsInput[i].index === join.index)
return this.lookupsInput[i].pgIndexId;
getValueForJoin(join) {
for(let lookup of this.lookups) {
if(lookup.index === join.index)
return lookup.pgIndexId;
}
return null;
},
setValueForJoin:function(join,pgIndexId) {
let pos = -1;
for(let i = 0, j = this.lookupsInput.length; i < j; i++) {
if(this.lookupsInput[i].index === join.index) {
pos = i;
setValueForJoin(join,pgIndexId) {
let lookups = JSON.parse(JSON.stringify(this.lookups));
for(let i = 0, j = lookups.length; i < j; i++) {
if(lookups[i].index === join.index) {
lookups.splice(i,1);
break;
}
}
if(pgIndexId === null && pos !== -1)
this.lookupsInput.splice(pos,1);
if(pgIndexId !== null && pos === -1)
this.lookupsInput.push({
if(pgIndexId !== null)
lookups.push({
pgIndexId:pgIndexId,
index:join.index
});
this.lookupsInput = this.lookupsInput;
this.$emit('update',lookups);
}
}
};
+1
View File
@@ -328,6 +328,7 @@ let MyBuilderRelation = {
<tr>
<th>{{ capGen.actions }}</th>
<th>{{ capApp.indexAttributes }}</th>
<th>{{ capApp.indexPrimaryKey }}</th>
<th>{{ capApp.indexAutoFki }}</th>
<th>{{ capApp.indexUnique }}</th>
</tr>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+3 -3
View File
@@ -516,8 +516,7 @@
"contentValue":"REST",
"hasDeleteHint":"Löscht einen bestehenden Datensatz - plus zusammenhängende Datensätze, wenn andere Relationen verbunden sind. Relationen müssen die Option \"Löschen\" aktiv haben, um berücksichtigt zu werden.",
"hasGetHint":"Liefert Werte von einem bestehenden Datensatz (wenn Datensatz-ID definiert ist) oder von allen verfügbaren Datensätzen von einer oder mehreren, verbundenen Relationen.",
"hasPatchHint":"Aktualisiert Werte für einen bestehenden Datensatz - plus von zusammenhängenden Datensätzen, wenn andere Relationen verbunden sind. Relationen müssen die Option \"Aktualisieren\" aktiv haben, um berücksichtigt zu werden.",
"hasPostHint":"Erstellt einen neuen Datensatz - plus zusammenhängende Datensätze, wenn andere Relationen verbunden sind. Relationen müssen die Option \"Erstellen\" aktiv haben, um berücksichtigt zu werden.",
"hasPostHint":"Erstellt oder aktualisiert einen Datensatz - plus zusammenhängende Datensätze, wenn andere Relationen verbunden sind. Relationen müssen die Option \"Erstellen\" aktiv haben, um berücksichtigt zu werden.",
"httpMethods":"HTTP-Methoden",
"limitDef":"GET Ergebnisanzahl",
"limitDefHint":"Anzahl der Ergebnisse wenn nicht überschrieben mit dem Getter \"limit\" (bspw. \"limit=1000\").",
@@ -1097,8 +1096,9 @@
"graphBase":"Basisrelation",
"indexes":"Indexe ({CNT})",
"indexAttributes":"Attribute",
"indexAutoFki":"System",
"indexAutoFki":"Beziehung",
"indexCreate":"Attribut neuen Index hinzufügen",
"indexPrimaryKey":"Primärer Schlüssel",
"indexUnique":"Einzigartig",
"isDeferred":"DEFERRED",
"nameHint":"Referenzname für diese Relation. Muss einzigartig in dieser Anwendung sein.",
+3 -3
View File
@@ -516,8 +516,7 @@
"contentValue":"REST",
"hasDeleteHint":"Deletes an existing record - plus connected records if other relations are joined. Relations need the 'DELETE' option enabled to be affected.",
"hasGetHint":"Returns values from an existing (if record ID is given) or from all available records from one or multiple, joined relations.",
"hasPatchHint":"Changes specific values for an existing record - plus of connected records if other relations are joined. Relations need the 'UPDATE' option enabled to be affected.",
"hasPostHint":"Creates a new record - plus connected records if other relations are joined. Relations need the 'CREATE' option enabled to be affected.",
"hasPostHint":"Creates or updates a record - plus connected records if other relations are joined. Relations need the 'CREATE' option enabled to be affected.",
"httpMethods":"HTTP methods",
"limitDef":"GET result count",
"limitDefHint":"Default result count if not overwritten with the 'limit' getter (such as 'limit=1000').",
@@ -1097,8 +1096,9 @@
"graphBase":"Base relation",
"indexes":"Indexes ({CNT})",
"indexAttributes":"Attributes",
"indexAutoFki":"System",
"indexAutoFki":"Relationship",
"indexCreate":"Add attribute to new index",
"indexPrimaryKey":"Primary key",
"indexUnique":"Unique",
"isDeferred":"DEFERRED",
"nameHint":"Reference name for this relation. Must be unique within this application.",
+3 -3
View File
@@ -516,8 +516,7 @@
"contentValue":"REST",
"hasDeleteHint":"Deletes an existing record - plus connected records if other relations are joined. Relations need the 'DELETE' option enabled to be affected.",
"hasGetHint":"Returns values from an existing (if record ID is given) or from all available records from one or multiple, joined relations.",
"hasPatchHint":"Changes specific values for an existing record - plus of connected records if other relations are joined. Relations need the 'UPDATE' option enabled to be affected.",
"hasPostHint":"Creates a new record - plus connected records if other relations are joined. Relations need the 'CREATE' option enabled to be affected.",
"hasPostHint":"Creates or updates a record - plus connected records if other relations are joined. Relations need the 'CREATE' option enabled to be affected.",
"httpMethods":"HTTP methods",
"limitDef":"GET result count",
"limitDefHint":"Default result count if not overwritten with the 'limit' getter (such as 'limit=1000').",
@@ -1097,8 +1096,9 @@
"graphBase":"relazione base",
"indexes":"Indici ({CNT})",
"indexAttributes":"Attributi",
"indexAutoFki":"Sistema",
"indexAutoFki":"Relationship",
"indexCreate":"Aggiungi attributo al nuovo indice",
"indexPrimaryKey":"Primary key",
"indexUnique":"Univoco",
"isDeferred":"DEFERRED",
"nameHint":"Reference name for this relation. Must be unique within this application.",
+3 -3
View File
@@ -516,8 +516,7 @@
"contentValue":"REST",
"hasDeleteHint":"Deletes an existing record - plus connected records if other relations are joined. Relations need the 'DELETE' option enabled to be affected.",
"hasGetHint":"Returns values from an existing (if record ID is given) or from all available records from one or multiple, joined relations.",
"hasPatchHint":"Changes specific values for an existing record - plus of connected records if other relations are joined. Relations need the 'UPDATE' option enabled to be affected.",
"hasPostHint":"Creates a new record - plus connected records if other relations are joined. Relations need the 'CREATE' option enabled to be affected.",
"hasPostHint":"Creates or updates a record - plus connected records if other relations are joined. Relations need the 'CREATE' option enabled to be affected.",
"httpMethods":"HTTP methods",
"limitDef":"GET result count",
"limitDefHint":"Default result count if not overwritten with the 'limit' getter (such as 'limit=1000').",
@@ -1097,8 +1096,9 @@
"graphBase":"Relația de bază",
"indexes":"Indexuri ({CNT})",
"indexAttributes":"Atribute",
"indexAutoFki":"Sistem",
"indexAutoFki":"Relationship",
"indexCreate":"Adăugați un atribut la noul index",
"indexPrimaryKey":"Primary key",
"indexUnique":"Unic",
"isDeferred":"AMÂNAT",
"nameHint":"Reference name for this relation. Must be unique within this application.",