mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-07-30 14:17:39 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 47708fc87a |
+442
-303
@@ -259,278 +259,315 @@ func (h *Handler) handleRead(ctx context.Context, w common.ResponseWriter, id st
|
|||||||
sliceType := reflect.SliceOf(reflect.PointerTo(modelType))
|
sliceType := reflect.SliceOf(reflect.PointerTo(modelType))
|
||||||
modelPtr := reflect.New(sliceType).Interface()
|
modelPtr := reflect.New(sliceType).Interface()
|
||||||
|
|
||||||
// Start with Model() using the slice pointer to avoid "Model(nil)" errors in Count()
|
// Everything below runs inside a single transaction so that the BeforeRead
|
||||||
// Bun's Model() accepts both single pointers and slice pointers
|
// hook (which sets session-scoped RLS GUCs via SET LOCAL) executes on the
|
||||||
query := h.db.NewSelect().Model(modelPtr)
|
// same physical connection as the queries it is meant to protect. Under
|
||||||
|
// connection pooling, firing the hook against h.db and then querying
|
||||||
|
// against h.db again may hand out two different connections, silently
|
||||||
|
// bypassing RLS.
|
||||||
|
var (
|
||||||
|
result interface{}
|
||||||
|
total int
|
||||||
|
rowNumber *int64
|
||||||
|
limit int
|
||||||
|
offset int
|
||||||
|
statusCode int
|
||||||
|
errCode string
|
||||||
|
errMsg string
|
||||||
|
)
|
||||||
|
|
||||||
// Only set Table() if the model doesn't provide a table name via the underlying type
|
txErr := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
// Create a temporary instance to check for TableNameProvider
|
hookCtx := &HookContext{
|
||||||
tempInstance := reflect.New(modelType).Interface()
|
Context: ctx,
|
||||||
if provider, ok := tempInstance.(common.TableNameProvider); !ok || provider.TableName() == "" {
|
Handler: h,
|
||||||
query = query.Table(tableName)
|
Schema: schema,
|
||||||
}
|
Entity: entity,
|
||||||
|
Model: model,
|
||||||
if len(options.Columns) == 0 && (len(options.ComputedColumns) > 0) {
|
Options: options,
|
||||||
logger.Debug("Populating options.Columns with all model columns since computed columns are additions")
|
ID: id,
|
||||||
options.Columns = reflection.GetSQLModelColumns(model)
|
Writer: w,
|
||||||
}
|
Tx: tx,
|
||||||
|
|
||||||
// Apply column selection
|
|
||||||
if len(options.Columns) > 0 {
|
|
||||||
logger.Debug("Selecting columns: %v", options.Columns)
|
|
||||||
for _, col := range options.Columns {
|
|
||||||
query = query.Column(reflection.ExtractSourceColumn(col))
|
|
||||||
}
|
}
|
||||||
}
|
if err := h.hooks.Execute(BeforeRead, hookCtx); err != nil {
|
||||||
|
statusCode, errCode, errMsg = http.StatusInternalServerError, "hook_error", "BeforeRead hook failed"
|
||||||
if len(options.ComputedColumns) > 0 {
|
return err
|
||||||
for _, cu := range options.ComputedColumns {
|
|
||||||
logger.Debug("Applying computed column: %s", cu.Name)
|
|
||||||
query = query.ColumnExpr(fmt.Sprintf("(%s) AS %s", cu.Expression, cu.Name))
|
|
||||||
}
|
}
|
||||||
}
|
options = hookCtx.Options
|
||||||
|
|
||||||
// Apply preloading
|
// Start with Model() using the slice pointer to avoid "Model(nil)" errors in Count()
|
||||||
if len(options.Preload) > 0 {
|
// Bun's Model() accepts both single pointers and slice pointers
|
||||||
var err error
|
query := tx.NewSelect().Model(modelPtr)
|
||||||
query, err = h.applyPreloads(model, query, options.Preload)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Failed to apply preloads: %v", err)
|
|
||||||
h.sendError(w, http.StatusBadRequest, "invalid_preload", "Failed to apply preloads", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply filters with proper grouping for OR logic
|
// Only set Table() if the model doesn't provide a table name via the underlying type
|
||||||
query = h.applyFilters(query, options.Filters)
|
// Create a temporary instance to check for TableNameProvider
|
||||||
|
tempInstance := reflect.New(modelType).Interface()
|
||||||
// Apply custom operators
|
if provider, ok := tempInstance.(common.TableNameProvider); !ok || provider.TableName() == "" {
|
||||||
for _, customOp := range options.CustomOperators {
|
query = query.Table(tableName)
|
||||||
logger.Debug("Applying custom operator: %s - %s", customOp.Name, customOp.SQL)
|
|
||||||
query = query.Where(customOp.SQL)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply sorting
|
|
||||||
for _, sort := range options.Sort {
|
|
||||||
direction := "ASC"
|
|
||||||
if strings.EqualFold(sort.Direction, "desc") {
|
|
||||||
direction = "DESC"
|
|
||||||
}
|
|
||||||
logger.Debug("Applying sort: %s %s", sort.Column, direction)
|
|
||||||
query = query.Order(fmt.Sprintf("%s %s", sort.Column, direction))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply cursor-based pagination
|
|
||||||
if len(options.CursorForward) > 0 || len(options.CursorBackward) > 0 {
|
|
||||||
logger.Debug("Applying cursor pagination")
|
|
||||||
|
|
||||||
// Get primary key name
|
|
||||||
pkName := reflection.GetPrimaryKeyName(model)
|
|
||||||
|
|
||||||
// Extract model columns for validation
|
|
||||||
modelColumns := reflection.GetModelColumns(model)
|
|
||||||
|
|
||||||
// Default sort to primary key when none provided
|
|
||||||
if len(options.Sort) == 0 {
|
|
||||||
options.Sort = []common.SortOption{{Column: pkName, Direction: "ASC"}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get cursor filter SQL (expandJoins is empty for resolvespec — no custom SQL join support yet)
|
if len(options.Columns) == 0 && (len(options.ComputedColumns) > 0) {
|
||||||
cursorFilter, err := GetCursorFilter(tableName, pkName, modelColumns, options, nil)
|
logger.Debug("Populating options.Columns with all model columns since computed columns are additions")
|
||||||
if err != nil {
|
options.Columns = reflection.GetSQLModelColumns(model)
|
||||||
logger.Error("Error building cursor filter: %v", err)
|
|
||||||
h.sendError(w, http.StatusBadRequest, "cursor_error", "Invalid cursor pagination", err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply cursor filter to query
|
// Apply column selection
|
||||||
if cursorFilter != "" {
|
if len(options.Columns) > 0 {
|
||||||
logger.Debug("Applying cursor filter: %s", cursorFilter)
|
logger.Debug("Selecting columns: %v", options.Columns)
|
||||||
sanitizedCursor := common.SanitizeWhereClause(cursorFilter, reflection.ExtractTableNameOnly(tableName), &options)
|
for _, col := range options.Columns {
|
||||||
// Ensure outer parentheses to prevent OR logic from escaping
|
query = query.Column(reflection.ExtractSourceColumn(col))
|
||||||
sanitizedCursor = common.EnsureOuterParentheses(sanitizedCursor)
|
|
||||||
if sanitizedCursor != "" {
|
|
||||||
query = query.Where(sanitizedCursor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Get total count before pagination
|
if len(options.ComputedColumns) > 0 {
|
||||||
var total int
|
for _, cu := range options.ComputedColumns {
|
||||||
|
logger.Debug("Applying computed column: %s", cu.Name)
|
||||||
// Try to get from cache first
|
query = query.ColumnExpr(fmt.Sprintf("(%s) AS %s", cu.Expression, cu.Name))
|
||||||
// Use extended cache key if cursors are present
|
|
||||||
var cacheKeyHash string
|
|
||||||
if len(options.CursorForward) > 0 || len(options.CursorBackward) > 0 {
|
|
||||||
cacheKeyHash = buildExtendedQueryCacheKey(
|
|
||||||
tableName,
|
|
||||||
options.Filters,
|
|
||||||
options.Sort,
|
|
||||||
"", // No custom SQL WHERE in resolvespec
|
|
||||||
"", // No custom SQL OR in resolvespec
|
|
||||||
options.CursorForward,
|
|
||||||
options.CursorBackward,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
cacheKeyHash = buildQueryCacheKey(
|
|
||||||
tableName,
|
|
||||||
options.Filters,
|
|
||||||
options.Sort,
|
|
||||||
"", // No custom SQL WHERE in resolvespec
|
|
||||||
"", // No custom SQL OR in resolvespec
|
|
||||||
)
|
|
||||||
}
|
|
||||||
cacheKey := getQueryTotalCacheKey(cacheKeyHash)
|
|
||||||
|
|
||||||
// Try to retrieve from cache
|
|
||||||
var cachedTotal cachedTotal
|
|
||||||
err := cache.GetDefaultCache().Get(ctx, cacheKey, &cachedTotal)
|
|
||||||
if err == nil {
|
|
||||||
total = cachedTotal.Total
|
|
||||||
logger.Debug("Total records (from cache): %d", total)
|
|
||||||
} else {
|
|
||||||
// Cache miss - execute count query
|
|
||||||
logger.Debug("Cache miss for query total")
|
|
||||||
count, err := query.Count(ctx)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Error counting records: %v", err)
|
|
||||||
h.sendError(w, http.StatusInternalServerError, "query_error", "Error counting records", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
total = count
|
|
||||||
logger.Debug("Total records (from query): %d", total)
|
|
||||||
|
|
||||||
// Store in cache with schema and table tags
|
|
||||||
cacheTTL := time.Minute * 2 // Default 2 minutes TTL
|
|
||||||
if err := setQueryTotalCache(ctx, cacheKey, total, schema, tableName, cacheTTL); err != nil {
|
|
||||||
logger.Warn("Failed to cache query total: %v", err)
|
|
||||||
// Don't fail the request if caching fails
|
|
||||||
} else {
|
|
||||||
logger.Debug("Cached query total with key: %s", cacheKey)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle FetchRowNumber if requested
|
|
||||||
var rowNumber *int64
|
|
||||||
if options.FetchRowNumber != nil && *options.FetchRowNumber != "" {
|
|
||||||
logger.Debug("Fetching row number for ID: %s", *options.FetchRowNumber)
|
|
||||||
pkName := reflection.GetPrimaryKeyName(model)
|
|
||||||
|
|
||||||
// Build ROW_NUMBER window function SQL
|
|
||||||
rowNumberSQL := "ROW_NUMBER() OVER ("
|
|
||||||
if len(options.Sort) > 0 {
|
|
||||||
rowNumberSQL += "ORDER BY "
|
|
||||||
for i, sort := range options.Sort {
|
|
||||||
if i > 0 {
|
|
||||||
rowNumberSQL += ", "
|
|
||||||
}
|
|
||||||
direction := "ASC"
|
|
||||||
if strings.EqualFold(sort.Direction, "desc") {
|
|
||||||
direction = "DESC"
|
|
||||||
}
|
|
||||||
rowNumberSQL += fmt.Sprintf("%s %s", sort.Column, direction)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rowNumberSQL += ")"
|
|
||||||
|
|
||||||
// Create a query to fetch the row number using a subquery approach
|
// Apply preloading
|
||||||
// We'll select the PK and row_number, then filter by the target ID
|
if len(options.Preload) > 0 {
|
||||||
type RowNumResult struct {
|
var err error
|
||||||
RowNum int64 `bun:"row_num"`
|
query, err = h.applyPreloads(model, query, options.Preload)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Failed to apply preloads: %v", err)
|
||||||
|
statusCode, errCode, errMsg = http.StatusBadRequest, "invalid_preload", "Failed to apply preloads"
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rowNumQuery := h.db.NewSelect().Table(tableName).
|
// Apply filters with proper grouping for OR logic
|
||||||
ColumnExpr(fmt.Sprintf("%s AS row_num", rowNumberSQL)).
|
query = h.applyFilters(query, options.Filters)
|
||||||
Column(pkName)
|
|
||||||
|
|
||||||
// Apply the same filters as the main query
|
|
||||||
for _, filter := range options.Filters {
|
|
||||||
rowNumQuery = h.applyFilter(rowNumQuery, filter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply custom operators
|
// Apply custom operators
|
||||||
for _, customOp := range options.CustomOperators {
|
for _, customOp := range options.CustomOperators {
|
||||||
rowNumQuery = rowNumQuery.Where(customOp.SQL)
|
logger.Debug("Applying custom operator: %s - %s", customOp.Name, customOp.SQL)
|
||||||
|
query = query.Where(customOp.SQL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter for the specific ID we want the row number for
|
// Apply sorting
|
||||||
rowNumQuery = rowNumQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), *options.FetchRowNumber)
|
for _, sort := range options.Sort {
|
||||||
|
direction := "ASC"
|
||||||
// Execute query to get row number
|
if strings.EqualFold(sort.Direction, "desc") {
|
||||||
var result RowNumResult
|
direction = "DESC"
|
||||||
if err := rowNumQuery.Scan(ctx, &result); err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
// Build filter description for error message
|
|
||||||
filterInfo := fmt.Sprintf("filters: %d", len(options.Filters))
|
|
||||||
if len(options.CustomOperators) > 0 {
|
|
||||||
customOps := make([]string, 0, len(options.CustomOperators))
|
|
||||||
for _, op := range options.CustomOperators {
|
|
||||||
customOps = append(customOps, op.SQL)
|
|
||||||
}
|
|
||||||
filterInfo += fmt.Sprintf(", custom operators: [%s]", strings.Join(customOps, "; "))
|
|
||||||
}
|
|
||||||
logger.Warn("No row found for primary key %s=%s with %s", pkName, *options.FetchRowNumber, filterInfo)
|
|
||||||
} else {
|
|
||||||
logger.Warn("Error fetching row number: %v", err)
|
|
||||||
}
|
}
|
||||||
|
logger.Debug("Applying sort: %s %s", sort.Column, direction)
|
||||||
|
query = query.Order(fmt.Sprintf("%s %s", sort.Column, direction))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply cursor-based pagination
|
||||||
|
if len(options.CursorForward) > 0 || len(options.CursorBackward) > 0 {
|
||||||
|
logger.Debug("Applying cursor pagination")
|
||||||
|
|
||||||
|
// Get primary key name
|
||||||
|
pkName := reflection.GetPrimaryKeyName(model)
|
||||||
|
|
||||||
|
// Extract model columns for validation
|
||||||
|
modelColumns := reflection.GetModelColumns(model)
|
||||||
|
|
||||||
|
// Default sort to primary key when none provided
|
||||||
|
if len(options.Sort) == 0 {
|
||||||
|
options.Sort = []common.SortOption{{Column: pkName, Direction: "ASC"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cursor filter SQL (expandJoins is empty for resolvespec — no custom SQL join support yet)
|
||||||
|
cursorFilter, err := GetCursorFilter(tableName, pkName, modelColumns, options, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Error building cursor filter: %v", err)
|
||||||
|
statusCode, errCode, errMsg = http.StatusBadRequest, "cursor_error", "Invalid cursor pagination"
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply cursor filter to query
|
||||||
|
if cursorFilter != "" {
|
||||||
|
logger.Debug("Applying cursor filter: %s", cursorFilter)
|
||||||
|
sanitizedCursor := common.SanitizeWhereClause(cursorFilter, reflection.ExtractTableNameOnly(tableName), &options)
|
||||||
|
// Ensure outer parentheses to prevent OR logic from escaping
|
||||||
|
sanitizedCursor = common.EnsureOuterParentheses(sanitizedCursor)
|
||||||
|
if sanitizedCursor != "" {
|
||||||
|
query = query.Where(sanitizedCursor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get from cache first
|
||||||
|
// Use extended cache key if cursors are present
|
||||||
|
var cacheKeyHash string
|
||||||
|
if len(options.CursorForward) > 0 || len(options.CursorBackward) > 0 {
|
||||||
|
cacheKeyHash = buildExtendedQueryCacheKey(
|
||||||
|
tableName,
|
||||||
|
options.Filters,
|
||||||
|
options.Sort,
|
||||||
|
"", // No custom SQL WHERE in resolvespec
|
||||||
|
"", // No custom SQL OR in resolvespec
|
||||||
|
options.CursorForward,
|
||||||
|
options.CursorBackward,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
rowNumber = &result.RowNum
|
cacheKeyHash = buildQueryCacheKey(
|
||||||
logger.Debug("Found row number: %d", *rowNumber)
|
tableName,
|
||||||
|
options.Filters,
|
||||||
|
options.Sort,
|
||||||
|
"", // No custom SQL WHERE in resolvespec
|
||||||
|
"", // No custom SQL OR in resolvespec
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
cacheKey := getQueryTotalCacheKey(cacheKeyHash)
|
||||||
|
|
||||||
// Apply pagination (skip if FetchRowNumber is set - we want only that record)
|
// Try to retrieve from cache
|
||||||
if options.FetchRowNumber == nil || *options.FetchRowNumber == "" {
|
var cachedTotal cachedTotal
|
||||||
if options.Limit != nil && *options.Limit > 0 {
|
if err := cache.GetDefaultCache().Get(ctx, cacheKey, &cachedTotal); err == nil {
|
||||||
logger.Debug("Applying limit: %d", *options.Limit)
|
total = cachedTotal.Total
|
||||||
query = query.Limit(*options.Limit)
|
logger.Debug("Total records (from cache): %d", total)
|
||||||
}
|
|
||||||
if options.Offset != nil && *options.Offset > 0 {
|
|
||||||
logger.Debug("Applying offset: %d", *options.Offset)
|
|
||||||
query = query.Offset(*options.Offset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute query
|
|
||||||
var result interface{}
|
|
||||||
if id != "" || (options.FetchRowNumber != nil && *options.FetchRowNumber != "") {
|
|
||||||
// Single record query - either by URL ID or FetchRowNumber
|
|
||||||
var targetID string
|
|
||||||
if id != "" {
|
|
||||||
targetID = id
|
|
||||||
logger.Debug("Querying single record with URL ID: %s", id)
|
|
||||||
} else {
|
} else {
|
||||||
targetID = *options.FetchRowNumber
|
// Cache miss - execute count query
|
||||||
logger.Debug("Querying single record with FetchRowNumber ID: %s", targetID)
|
logger.Debug("Cache miss for query total")
|
||||||
|
count, err := query.Count(ctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Error counting records: %v", err)
|
||||||
|
statusCode, errCode, errMsg = http.StatusInternalServerError, "query_error", "Error counting records"
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
total = count
|
||||||
|
logger.Debug("Total records (from query): %d", total)
|
||||||
|
|
||||||
|
// Store in cache with schema and table tags
|
||||||
|
cacheTTL := time.Minute * 2 // Default 2 minutes TTL
|
||||||
|
if err := setQueryTotalCache(ctx, cacheKey, total, schema, tableName, cacheTTL); err != nil {
|
||||||
|
logger.Warn("Failed to cache query total: %v", err)
|
||||||
|
// Don't fail the request if caching fails
|
||||||
|
} else {
|
||||||
|
logger.Debug("Cached query total with key: %s", cacheKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For single record, create a new pointer to the struct type
|
// Handle FetchRowNumber if requested
|
||||||
singleResult := reflect.New(modelType).Interface()
|
if options.FetchRowNumber != nil && *options.FetchRowNumber != "" {
|
||||||
pkName := reflection.GetPrimaryKeyName(singleResult)
|
logger.Debug("Fetching row number for ID: %s", *options.FetchRowNumber)
|
||||||
|
pkName := reflection.GetPrimaryKeyName(model)
|
||||||
|
|
||||||
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
|
// Build ROW_NUMBER window function SQL
|
||||||
if err := query.Scan(ctx, singleResult); err != nil {
|
rowNumberSQL := "ROW_NUMBER() OVER ("
|
||||||
logger.Error("Error querying record: %v", err)
|
if len(options.Sort) > 0 {
|
||||||
h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err)
|
rowNumberSQL += "ORDER BY "
|
||||||
return
|
for i, sort := range options.Sort {
|
||||||
|
if i > 0 {
|
||||||
|
rowNumberSQL += ", "
|
||||||
|
}
|
||||||
|
direction := "ASC"
|
||||||
|
if strings.EqualFold(sort.Direction, "desc") {
|
||||||
|
direction = "DESC"
|
||||||
|
}
|
||||||
|
rowNumberSQL += fmt.Sprintf("%s %s", sort.Column, direction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rowNumberSQL += ")"
|
||||||
|
|
||||||
|
// Create a query to fetch the row number using a subquery approach
|
||||||
|
// We'll select the PK and row_number, then filter by the target ID
|
||||||
|
type RowNumResult struct {
|
||||||
|
RowNum int64 `bun:"row_num"`
|
||||||
|
}
|
||||||
|
|
||||||
|
rowNumQuery := tx.NewSelect().Table(tableName).
|
||||||
|
ColumnExpr(fmt.Sprintf("%s AS row_num", rowNumberSQL)).
|
||||||
|
Column(pkName)
|
||||||
|
|
||||||
|
// Apply the same filters as the main query
|
||||||
|
for _, filter := range options.Filters {
|
||||||
|
rowNumQuery = h.applyFilter(rowNumQuery, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply custom operators
|
||||||
|
for _, customOp := range options.CustomOperators {
|
||||||
|
rowNumQuery = rowNumQuery.Where(customOp.SQL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter for the specific ID we want the row number for
|
||||||
|
rowNumQuery = rowNumQuery.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), *options.FetchRowNumber)
|
||||||
|
|
||||||
|
// Execute query to get row number
|
||||||
|
var rnResult RowNumResult
|
||||||
|
if err := rowNumQuery.Scan(ctx, &rnResult); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
// Build filter description for error message
|
||||||
|
filterInfo := fmt.Sprintf("filters: %d", len(options.Filters))
|
||||||
|
if len(options.CustomOperators) > 0 {
|
||||||
|
customOps := make([]string, 0, len(options.CustomOperators))
|
||||||
|
for _, op := range options.CustomOperators {
|
||||||
|
customOps = append(customOps, op.SQL)
|
||||||
|
}
|
||||||
|
filterInfo += fmt.Sprintf(", custom operators: [%s]", strings.Join(customOps, "; "))
|
||||||
|
}
|
||||||
|
logger.Warn("No row found for primary key %s=%s with %s", pkName, *options.FetchRowNumber, filterInfo)
|
||||||
|
} else {
|
||||||
|
logger.Warn("Error fetching row number: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rowNumber = &rnResult.RowNum
|
||||||
|
logger.Debug("Found row number: %d", *rowNumber)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result = singleResult
|
|
||||||
} else {
|
// Apply pagination (skip if FetchRowNumber is set - we want only that record)
|
||||||
logger.Debug("Querying multiple records")
|
if options.FetchRowNumber == nil || *options.FetchRowNumber == "" {
|
||||||
// Use the modelPtr already created and set on the query
|
if options.Limit != nil && *options.Limit > 0 {
|
||||||
if err := query.Scan(ctx, modelPtr); err != nil {
|
logger.Debug("Applying limit: %d", *options.Limit)
|
||||||
logger.Error("Error querying records: %v", err)
|
query = query.Limit(*options.Limit)
|
||||||
h.sendError(w, http.StatusInternalServerError, "query_error", "Error executing query", err)
|
}
|
||||||
return
|
if options.Offset != nil && *options.Offset > 0 {
|
||||||
|
logger.Debug("Applying offset: %d", *options.Offset)
|
||||||
|
query = query.Offset(*options.Offset)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result = reflect.ValueOf(modelPtr).Elem().Interface()
|
|
||||||
|
// Execute query
|
||||||
|
if id != "" || (options.FetchRowNumber != nil && *options.FetchRowNumber != "") {
|
||||||
|
// Single record query - either by URL ID or FetchRowNumber
|
||||||
|
var targetID string
|
||||||
|
if id != "" {
|
||||||
|
targetID = id
|
||||||
|
logger.Debug("Querying single record with URL ID: %s", id)
|
||||||
|
} else {
|
||||||
|
targetID = *options.FetchRowNumber
|
||||||
|
logger.Debug("Querying single record with FetchRowNumber ID: %s", targetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For single record, create a new pointer to the struct type
|
||||||
|
singleResult := reflect.New(modelType).Interface()
|
||||||
|
pkName := reflection.GetPrimaryKeyName(singleResult)
|
||||||
|
|
||||||
|
query = query.Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), targetID)
|
||||||
|
if err := query.Scan(ctx, singleResult); err != nil {
|
||||||
|
logger.Error("Error querying record: %v", err)
|
||||||
|
statusCode, errCode, errMsg = http.StatusInternalServerError, "query_error", "Error executing query"
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result = singleResult
|
||||||
|
} else {
|
||||||
|
logger.Debug("Querying multiple records")
|
||||||
|
// Use the modelPtr already created and set on the query
|
||||||
|
if err := query.Scan(ctx, modelPtr); err != nil {
|
||||||
|
logger.Error("Error querying records: %v", err)
|
||||||
|
statusCode, errCode, errMsg = http.StatusInternalServerError, "query_error", "Error executing query"
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
result = reflect.ValueOf(modelPtr).Elem().Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("Successfully retrieved records")
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if txErr != nil {
|
||||||
|
if statusCode == 0 {
|
||||||
|
statusCode, errCode, errMsg = http.StatusInternalServerError, "query_error", "Error executing query"
|
||||||
|
}
|
||||||
|
h.sendError(w, statusCode, errCode, errMsg, txErr)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Successfully retrieved records")
|
|
||||||
|
|
||||||
// Build metadata
|
// Build metadata
|
||||||
limit := 0
|
|
||||||
offset := 0
|
|
||||||
count := int64(total)
|
count := int64(total)
|
||||||
|
|
||||||
// When FetchRowNumber is used, we only return 1 record
|
// When FetchRowNumber is used, we only return 1 record
|
||||||
@@ -585,54 +622,107 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
// Check if we should use nested processing
|
// Check if we should use nested processing
|
||||||
if h.shouldUseNestedProcessor(v, model) {
|
if h.shouldUseNestedProcessor(v, model) {
|
||||||
logger.Info("Using nested CUD processor for create operation")
|
logger.Info("Using nested CUD processor for create operation")
|
||||||
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "insert", v, model, make(map[string]interface{}), tableName)
|
var nestedResult *common.ProcessResult
|
||||||
|
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
|
hookCtx := &HookContext{
|
||||||
|
Context: ctx,
|
||||||
|
Handler: h,
|
||||||
|
Schema: schema,
|
||||||
|
Entity: entity,
|
||||||
|
Model: model,
|
||||||
|
Options: options,
|
||||||
|
Data: v,
|
||||||
|
Writer: w,
|
||||||
|
Tx: tx,
|
||||||
|
}
|
||||||
|
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
||||||
|
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
||||||
|
}
|
||||||
|
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
|
v = modifiedData
|
||||||
|
}
|
||||||
|
|
||||||
|
originalProcessor := h.nestedProcessor
|
||||||
|
h.nestedProcessor = common.NewNestedCUDProcessor(tx, h.registry, h)
|
||||||
|
defer func() {
|
||||||
|
h.nestedProcessor = originalProcessor
|
||||||
|
}()
|
||||||
|
|
||||||
|
var procErr error
|
||||||
|
nestedResult, procErr = h.nestedProcessor.ProcessNestedCUD(ctx, "insert", v, model, make(map[string]interface{}), tableName)
|
||||||
|
return procErr
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Error in nested create: %v", err)
|
logger.Error("Error in nested create: %v", err)
|
||||||
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating record with nested data", err)
|
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating record with nested data", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Info("Successfully created record with nested data, ID: %v", result.ID)
|
logger.Info("Successfully created record with nested data, ID: %v", nestedResult.ID)
|
||||||
// Invalidate cache for this table
|
// Invalidate cache for this table
|
||||||
cacheTags := buildCacheTags(schema, tableName)
|
cacheTags := buildCacheTags(schema, tableName)
|
||||||
if err := invalidateCacheForTags(ctx, cacheTags); err != nil {
|
if err := invalidateCacheForTags(ctx, cacheTags); err != nil {
|
||||||
logger.Warn("Failed to invalidate cache for table %s: %v", tableName, err)
|
logger.Warn("Failed to invalidate cache for table %s: %v", tableName, err)
|
||||||
}
|
}
|
||||||
h.sendResponse(w, result.Data, nil)
|
h.sendResponse(w, nestedResult.Data, nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Standard processing without nested relations
|
// Standard processing without nested relations
|
||||||
pkName := reflection.GetPrimaryKeyName(model)
|
pkName := reflection.GetPrimaryKeyName(model)
|
||||||
query := h.db.NewInsert().Table(tableName)
|
|
||||||
for key, value := range v {
|
|
||||||
query = query.Value(key, common.ConvertSliceForBun(value))
|
|
||||||
}
|
|
||||||
var responseData interface{} = v
|
var responseData interface{} = v
|
||||||
if pkName == "" {
|
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
// No PK on model — insert and return input as-is.
|
hookCtx := &HookContext{
|
||||||
result, err := query.Exec(ctx)
|
Context: ctx,
|
||||||
if err != nil {
|
Handler: h,
|
||||||
logger.Error("Error creating record: %v", err)
|
Schema: schema,
|
||||||
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating record", err)
|
Entity: entity,
|
||||||
return
|
Model: model,
|
||||||
|
Options: options,
|
||||||
|
Data: v,
|
||||||
|
Writer: w,
|
||||||
|
Tx: tx,
|
||||||
}
|
}
|
||||||
logger.Info("Successfully created record, rows affected: %d", result.RowsAffected())
|
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
||||||
} else {
|
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
||||||
|
}
|
||||||
|
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
|
v = modifiedData
|
||||||
|
}
|
||||||
|
responseData = v
|
||||||
|
|
||||||
|
query := tx.NewInsert().Table(tableName)
|
||||||
|
for key, value := range v {
|
||||||
|
query = query.Value(key, common.ConvertSliceForBun(value))
|
||||||
|
}
|
||||||
|
if pkName == "" {
|
||||||
|
// No PK on model — insert and return input as-is.
|
||||||
|
result, err := query.Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info("Successfully created record, rows affected: %d", result.RowsAffected())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var insertedID interface{}
|
var insertedID interface{}
|
||||||
if err := query.Returning(pkName).Scan(ctx, &insertedID); err != nil {
|
if err := query.Returning(pkName).Scan(ctx, &insertedID); err != nil {
|
||||||
logger.Error("Error creating record: %v", err)
|
return err
|
||||||
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating record", err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
logger.Info("Successfully created record with %s: %v", pkName, insertedID)
|
logger.Info("Successfully created record with %s: %v", pkName, insertedID)
|
||||||
fetchedRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
fetchedRecord := reflect.New(reflection.GetPointerElement(reflect.TypeOf(model))).Interface()
|
||||||
if fetchErr := h.db.NewSelect().Model(fetchedRecord).
|
if fetchErr := tx.NewSelect().Model(fetchedRecord).
|
||||||
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), insertedID).
|
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), insertedID).
|
||||||
ScanModel(ctx); fetchErr == nil {
|
ScanModel(ctx); fetchErr == nil {
|
||||||
responseData = mergeWithInput(fetchedRecord, v)
|
responseData = mergeWithInput(fetchedRecord, v)
|
||||||
} else {
|
} else {
|
||||||
logger.Warn("Failed to re-fetch created record with %s=%v: %v", pkName, insertedID, fetchErr)
|
logger.Warn("Failed to re-fetch created record with %s=%v: %v", pkName, insertedID, fetchErr)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Error creating record: %v", err)
|
||||||
|
h.sendError(w, http.StatusInternalServerError, "create_error", "Error creating record", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
// Invalidate cache for this table
|
// Invalidate cache for this table
|
||||||
cacheTags := buildCacheTags(schema, tableName)
|
cacheTags := buildCacheTags(schema, tableName)
|
||||||
@@ -663,6 +753,24 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
for _, item := range v {
|
for _, item := range v {
|
||||||
|
hookCtx := &HookContext{
|
||||||
|
Context: ctx,
|
||||||
|
Handler: h,
|
||||||
|
Schema: schema,
|
||||||
|
Entity: entity,
|
||||||
|
Model: model,
|
||||||
|
Options: options,
|
||||||
|
Data: item,
|
||||||
|
Writer: w,
|
||||||
|
Tx: tx,
|
||||||
|
}
|
||||||
|
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
||||||
|
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
||||||
|
}
|
||||||
|
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
|
item = modifiedData
|
||||||
|
}
|
||||||
|
|
||||||
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "insert", item, model, make(map[string]interface{}), tableName)
|
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "insert", item, model, make(map[string]interface{}), tableName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to process item: %w", err)
|
return fmt.Errorf("failed to process item: %w", err)
|
||||||
@@ -689,10 +797,27 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
// Standard batch insert without nested relations
|
// Standard batch insert without nested relations
|
||||||
pkName := reflection.GetPrimaryKeyName(model)
|
pkName := reflection.GetPrimaryKeyName(model)
|
||||||
modelElemType := reflection.GetPointerElement(reflect.TypeOf(model))
|
modelElemType := reflection.GetPointerElement(reflect.TypeOf(model))
|
||||||
originals := make([]map[string]interface{}, 0, len(v))
|
responseItems := make([]interface{}, 0, len(v))
|
||||||
insertedIDs := make([]interface{}, 0, len(v))
|
|
||||||
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
for _, item := range v {
|
for _, item := range v {
|
||||||
|
hookCtx := &HookContext{
|
||||||
|
Context: ctx,
|
||||||
|
Handler: h,
|
||||||
|
Schema: schema,
|
||||||
|
Entity: entity,
|
||||||
|
Model: model,
|
||||||
|
Options: options,
|
||||||
|
Data: item,
|
||||||
|
Writer: w,
|
||||||
|
Tx: tx,
|
||||||
|
}
|
||||||
|
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
||||||
|
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
||||||
|
}
|
||||||
|
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
|
item = modifiedData
|
||||||
|
}
|
||||||
|
|
||||||
txQuery := tx.NewInsert().Table(tableName)
|
txQuery := tx.NewInsert().Table(tableName)
|
||||||
for key, value := range item {
|
for key, value := range item {
|
||||||
txQuery = txQuery.Value(key, common.ConvertSliceForBun(value))
|
txQuery = txQuery.Value(key, common.ConvertSliceForBun(value))
|
||||||
@@ -701,16 +826,22 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
if _, err := txQuery.Exec(ctx); err != nil {
|
if _, err := txQuery.Exec(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
originals = append(originals, item)
|
responseItems = append(responseItems, item)
|
||||||
insertedIDs = append(insertedIDs, nil)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var returnedID interface{}
|
var returnedID interface{}
|
||||||
if err := txQuery.Returning(pkName).Scan(ctx, &returnedID); err != nil {
|
if err := txQuery.Returning(pkName).Scan(ctx, &returnedID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
originals = append(originals, item)
|
fetchedRecord := reflect.New(modelElemType).Interface()
|
||||||
insertedIDs = append(insertedIDs, returnedID)
|
if fetchErr := tx.NewSelect().Model(fetchedRecord).
|
||||||
|
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), returnedID).
|
||||||
|
ScanModel(ctx); fetchErr == nil {
|
||||||
|
responseItems = append(responseItems, mergeWithInput(fetchedRecord, item))
|
||||||
|
} else {
|
||||||
|
logger.Warn("Failed to re-fetch created record with %s=%v: %v", pkName, returnedID, fetchErr)
|
||||||
|
responseItems = append(responseItems, item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -725,23 +856,6 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
if err := invalidateCacheForTags(ctx, cacheTags); err != nil {
|
if err := invalidateCacheForTags(ctx, cacheTags); err != nil {
|
||||||
logger.Warn("Failed to invalidate cache for table %s: %v", tableName, err)
|
logger.Warn("Failed to invalidate cache for table %s: %v", tableName, err)
|
||||||
}
|
}
|
||||||
// Re-fetch each record after transaction commits; fall back to input on failure.
|
|
||||||
responseItems := make([]interface{}, 0, len(insertedIDs))
|
|
||||||
for i, pkVal := range insertedIDs {
|
|
||||||
if pkVal == nil {
|
|
||||||
responseItems = append(responseItems, originals[i])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fetchedRecord := reflect.New(modelElemType).Interface()
|
|
||||||
if fetchErr := h.db.NewSelect().Model(fetchedRecord).
|
|
||||||
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), pkVal).
|
|
||||||
ScanModel(ctx); fetchErr == nil {
|
|
||||||
responseItems = append(responseItems, mergeWithInput(fetchedRecord, originals[i]))
|
|
||||||
} else {
|
|
||||||
logger.Warn("Failed to re-fetch created record with %s=%v: %v", pkName, pkVal, fetchErr)
|
|
||||||
responseItems = append(responseItems, originals[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h.sendResponse(w, responseItems, nil)
|
h.sendResponse(w, responseItems, nil)
|
||||||
|
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
@@ -770,6 +884,24 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
|
|
||||||
for _, item := range v {
|
for _, item := range v {
|
||||||
if itemMap, ok := item.(map[string]interface{}); ok {
|
if itemMap, ok := item.(map[string]interface{}); ok {
|
||||||
|
hookCtx := &HookContext{
|
||||||
|
Context: ctx,
|
||||||
|
Handler: h,
|
||||||
|
Schema: schema,
|
||||||
|
Entity: entity,
|
||||||
|
Model: model,
|
||||||
|
Options: options,
|
||||||
|
Data: itemMap,
|
||||||
|
Writer: w,
|
||||||
|
Tx: tx,
|
||||||
|
}
|
||||||
|
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
||||||
|
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
||||||
|
}
|
||||||
|
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
|
itemMap = modifiedData
|
||||||
|
}
|
||||||
|
|
||||||
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "insert", itemMap, model, make(map[string]interface{}), tableName)
|
result, err := h.nestedProcessor.ProcessNestedCUD(ctx, "insert", itemMap, model, make(map[string]interface{}), tableName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to process item: %w", err)
|
return fmt.Errorf("failed to process item: %w", err)
|
||||||
@@ -797,14 +929,32 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
// Standard batch insert without nested relations
|
// Standard batch insert without nested relations
|
||||||
pkName := reflection.GetPrimaryKeyName(model)
|
pkName := reflection.GetPrimaryKeyName(model)
|
||||||
modelElemType := reflection.GetPointerElement(reflect.TypeOf(model))
|
modelElemType := reflection.GetPointerElement(reflect.TypeOf(model))
|
||||||
originals := make([]map[string]interface{}, 0, len(v))
|
responseItems := make([]interface{}, 0, len(v))
|
||||||
insertedIDs := make([]interface{}, 0, len(v))
|
|
||||||
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
err := h.db.RunInTransaction(ctx, func(tx common.Database) error {
|
||||||
for _, item := range v {
|
for _, item := range v {
|
||||||
itemMap, ok := item.(map[string]interface{})
|
itemMap, ok := item.(map[string]interface{})
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hookCtx := &HookContext{
|
||||||
|
Context: ctx,
|
||||||
|
Handler: h,
|
||||||
|
Schema: schema,
|
||||||
|
Entity: entity,
|
||||||
|
Model: model,
|
||||||
|
Options: options,
|
||||||
|
Data: itemMap,
|
||||||
|
Writer: w,
|
||||||
|
Tx: tx,
|
||||||
|
}
|
||||||
|
if err := h.hooks.Execute(BeforeCreate, hookCtx); err != nil {
|
||||||
|
return fmt.Errorf("BeforeCreate hook failed: %w", err)
|
||||||
|
}
|
||||||
|
if modifiedData, ok := hookCtx.Data.(map[string]interface{}); ok {
|
||||||
|
itemMap = modifiedData
|
||||||
|
}
|
||||||
|
|
||||||
txQuery := tx.NewInsert().Table(tableName)
|
txQuery := tx.NewInsert().Table(tableName)
|
||||||
for key, value := range itemMap {
|
for key, value := range itemMap {
|
||||||
txQuery = txQuery.Value(key, common.ConvertSliceForBun(value))
|
txQuery = txQuery.Value(key, common.ConvertSliceForBun(value))
|
||||||
@@ -813,16 +963,22 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
if _, err := txQuery.Exec(ctx); err != nil {
|
if _, err := txQuery.Exec(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
originals = append(originals, itemMap)
|
responseItems = append(responseItems, itemMap)
|
||||||
insertedIDs = append(insertedIDs, nil)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var returnedID interface{}
|
var returnedID interface{}
|
||||||
if err := txQuery.Returning(pkName).Scan(ctx, &returnedID); err != nil {
|
if err := txQuery.Returning(pkName).Scan(ctx, &returnedID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
originals = append(originals, itemMap)
|
fetchedRecord := reflect.New(modelElemType).Interface()
|
||||||
insertedIDs = append(insertedIDs, returnedID)
|
if fetchErr := tx.NewSelect().Model(fetchedRecord).
|
||||||
|
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), returnedID).
|
||||||
|
ScanModel(ctx); fetchErr == nil {
|
||||||
|
responseItems = append(responseItems, mergeWithInput(fetchedRecord, itemMap))
|
||||||
|
} else {
|
||||||
|
logger.Warn("Failed to re-fetch created record with %s=%v: %v", pkName, returnedID, fetchErr)
|
||||||
|
responseItems = append(responseItems, itemMap)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -837,23 +993,6 @@ func (h *Handler) handleCreate(ctx context.Context, w common.ResponseWriter, dat
|
|||||||
if err := invalidateCacheForTags(ctx, cacheTags); err != nil {
|
if err := invalidateCacheForTags(ctx, cacheTags); err != nil {
|
||||||
logger.Warn("Failed to invalidate cache for table %s: %v", tableName, err)
|
logger.Warn("Failed to invalidate cache for table %s: %v", tableName, err)
|
||||||
}
|
}
|
||||||
// Re-fetch each record after transaction commits; fall back to input on failure.
|
|
||||||
responseItems := make([]interface{}, 0, len(insertedIDs))
|
|
||||||
for i, pkVal := range insertedIDs {
|
|
||||||
if pkVal == nil {
|
|
||||||
responseItems = append(responseItems, originals[i])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fetchedRecord := reflect.New(modelElemType).Interface()
|
|
||||||
if fetchErr := h.db.NewSelect().Model(fetchedRecord).
|
|
||||||
Where(fmt.Sprintf("%s = ?", common.QuoteIdent(pkName)), pkVal).
|
|
||||||
ScanModel(ctx); fetchErr == nil {
|
|
||||||
responseItems = append(responseItems, mergeWithInput(fetchedRecord, originals[i]))
|
|
||||||
} else {
|
|
||||||
logger.Warn("Failed to re-fetch created record with %s=%v: %v", pkName, pkVal, fetchErr)
|
|
||||||
responseItems = append(responseItems, originals[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h.sendResponse(w, responseItems, nil)
|
h.sendResponse(w, responseItems, nil)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
+468
-429
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user