mirror of
https://github.com/r3-team/r3.git
synced 2026-05-07 17:09:28 -05:00
API features/fixes
* Enabled use of PK attributes as lookups. * Improved API call handling. * API Builder improvements.
This commit is contained in:
+5
-1
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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'">
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 |
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user