fix: Enhance Kol Variable Rendering and Management
- Updated the regex for placeholder matching to support more flexible key formats. - Refactored variable storage to allow both ID and key lookups in rendering. - Improved schema validation to check for duplicate keys and enforce non-empty keys. - Added new utility functions for variable value lookup and display name generation. - Enhanced tests to cover new key-based variable scenarios. - Refactored KolPrompt repository to simplify prompt storage and management, including the removal of obsolete revision handling. - Introduced new API endpoints for saving, activating, and archiving prompts. - Added new utility functions for handling Kol placeholders and platform options in the admin web. - Implemented database migrations to simplify prompt storage structure and ensure data integrity.
This commit is contained in:
@@ -21,24 +21,15 @@ type KolPrompt struct {
|
||||
SortOrder int
|
||||
PublishedRevisionNo *int
|
||||
DraftRevisionNo *int
|
||||
PromptAssetKey *string
|
||||
SchemaJSON []byte
|
||||
CardConfigJSON []byte
|
||||
CreatedBy int64
|
||||
UpdatedBy int64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type KolPromptRevision struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
PromptID int64
|
||||
RevisionNo int
|
||||
PromptAssetKey string
|
||||
SchemaJSON []byte
|
||||
CardConfigJSON []byte
|
||||
CreatedBy int64
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type ActiveKolPrompt struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
@@ -64,14 +55,31 @@ type UpdateKolPromptMetadataInput struct {
|
||||
UpdatedBy int64
|
||||
}
|
||||
|
||||
type CreateKolPromptRevisionInput struct {
|
||||
TenantID int64
|
||||
PromptID int64
|
||||
RevisionNo int
|
||||
PromptAssetKey string
|
||||
SchemaJSON []byte
|
||||
CardConfigJSON []byte
|
||||
CreatedBy int64
|
||||
type SaveKolPromptInput struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
Name string
|
||||
PlatformHint *string
|
||||
SortOrder int
|
||||
PromptAssetKey string
|
||||
SchemaJSON []byte
|
||||
CardConfigJSON []byte
|
||||
Status string
|
||||
PublishedRevisionNo *int
|
||||
UpdatedBy int64
|
||||
}
|
||||
|
||||
type PublishKolPromptInput struct {
|
||||
ID int64
|
||||
TenantID int64
|
||||
Name string
|
||||
PlatformHint *string
|
||||
SortOrder int
|
||||
PromptAssetKey string
|
||||
SchemaJSON []byte
|
||||
CardConfigJSON []byte
|
||||
PublishedRevisionNo int
|
||||
UpdatedBy int64
|
||||
}
|
||||
|
||||
type KolPromptRepository interface {
|
||||
@@ -80,41 +88,121 @@ type KolPromptRepository interface {
|
||||
ListByPackage(ctx context.Context, tenantID, packageID int64) ([]KolPrompt, error)
|
||||
ListActiveByPackage(ctx context.Context, tenantID, packageID int64) ([]ActiveKolPrompt, error)
|
||||
UpdateMetadata(ctx context.Context, input UpdateKolPromptMetadataInput) error
|
||||
SetDraftRevision(ctx context.Context, tenantID, promptID int64, revNo int, updatedBy int64) error
|
||||
Save(ctx context.Context, input SaveKolPromptInput) error
|
||||
Publish(ctx context.Context, input PublishKolPromptInput) error
|
||||
SetPublishedRevision(ctx context.Context, tenantID, promptID int64, revNo int, updatedBy int64) error
|
||||
UpdateStatus(ctx context.Context, tenantID, id int64, status string, updatedBy int64) error
|
||||
SoftDelete(ctx context.Context, tenantID, id int64) error
|
||||
CreateRevision(ctx context.Context, input CreateKolPromptRevisionInput) (*KolPromptRevision, error)
|
||||
GetRevision(ctx context.Context, tenantID, promptID int64, revNo int) (*KolPromptRevision, error)
|
||||
NextRevisionNo(ctx context.Context, tenantID, promptID int64) (int, error)
|
||||
}
|
||||
|
||||
type kolPromptRepository struct {
|
||||
q generated.Querier
|
||||
db generated.DBTX
|
||||
q generated.Querier
|
||||
}
|
||||
|
||||
func NewKolPromptRepository(db generated.DBTX) KolPromptRepository {
|
||||
return &kolPromptRepository{q: newQuerier(db)}
|
||||
return &kolPromptRepository{db: db, q: newQuerier(db)}
|
||||
}
|
||||
|
||||
func mapKolPrompt(row generated.GetKolPromptByIDRow) *KolPrompt {
|
||||
func mapKolPromptRow(
|
||||
id int64,
|
||||
tenantID int64,
|
||||
packageID int64,
|
||||
name string,
|
||||
platformHint pgtype.Text,
|
||||
status string,
|
||||
sortOrder int32,
|
||||
publishedRevisionNo pgtype.Int4,
|
||||
draftRevisionNo pgtype.Int4,
|
||||
promptAssetKey pgtype.Text,
|
||||
schemaJSON []byte,
|
||||
cardConfigJSON []byte,
|
||||
createdBy int64,
|
||||
updatedBy int64,
|
||||
createdAt pgtype.Timestamptz,
|
||||
updatedAt pgtype.Timestamptz,
|
||||
) *KolPrompt {
|
||||
return &KolPrompt{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
PackageID: row.PackageID,
|
||||
Name: row.Name,
|
||||
PlatformHint: nullableText(row.PlatformHint),
|
||||
Status: row.Status,
|
||||
SortOrder: int(row.SortOrder),
|
||||
PublishedRevisionNo: nullableIntFromInt4(row.PublishedRevisionNo),
|
||||
DraftRevisionNo: nullableIntFromInt4(row.DraftRevisionNo),
|
||||
CreatedBy: row.CreatedBy,
|
||||
UpdatedBy: row.UpdatedBy,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
PackageID: packageID,
|
||||
Name: name,
|
||||
PlatformHint: nullableText(platformHint),
|
||||
Status: status,
|
||||
SortOrder: int(sortOrder),
|
||||
PublishedRevisionNo: nullableIntFromInt4(publishedRevisionNo),
|
||||
DraftRevisionNo: nullableIntFromInt4(draftRevisionNo),
|
||||
PromptAssetKey: nullableText(promptAssetKey),
|
||||
SchemaJSON: schemaJSON,
|
||||
CardConfigJSON: cardConfigJSON,
|
||||
CreatedBy: createdBy,
|
||||
UpdatedBy: updatedBy,
|
||||
CreatedAt: timeFromTimestamp(createdAt),
|
||||
UpdatedAt: timeFromTimestamp(updatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func scanKolPrompt(scanner interface{ Scan(...any) error }) (*KolPrompt, error) {
|
||||
var (
|
||||
id int64
|
||||
tenantID int64
|
||||
packageID int64
|
||||
name string
|
||||
platformHint pgtype.Text
|
||||
status string
|
||||
sortOrder int32
|
||||
publishedRevisionNo pgtype.Int4
|
||||
draftRevisionNo pgtype.Int4
|
||||
promptAssetKey pgtype.Text
|
||||
schemaJSON []byte
|
||||
cardConfigJSON []byte
|
||||
createdBy int64
|
||||
updatedBy int64
|
||||
createdAt pgtype.Timestamptz
|
||||
updatedAt pgtype.Timestamptz
|
||||
)
|
||||
|
||||
if err := scanner.Scan(
|
||||
&id,
|
||||
&tenantID,
|
||||
&packageID,
|
||||
&name,
|
||||
&platformHint,
|
||||
&status,
|
||||
&sortOrder,
|
||||
&publishedRevisionNo,
|
||||
&draftRevisionNo,
|
||||
&promptAssetKey,
|
||||
&schemaJSON,
|
||||
&cardConfigJSON,
|
||||
&createdBy,
|
||||
&updatedBy,
|
||||
&createdAt,
|
||||
&updatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mapKolPromptRow(
|
||||
id,
|
||||
tenantID,
|
||||
packageID,
|
||||
name,
|
||||
platformHint,
|
||||
status,
|
||||
sortOrder,
|
||||
publishedRevisionNo,
|
||||
draftRevisionNo,
|
||||
promptAssetKey,
|
||||
schemaJSON,
|
||||
cardConfigJSON,
|
||||
createdBy,
|
||||
updatedBy,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
), nil
|
||||
}
|
||||
|
||||
func (r *kolPromptRepository) Create(ctx context.Context, input CreateKolPromptInput) (*KolPrompt, error) {
|
||||
row, err := r.q.CreateKolPrompt(ctx, generated.CreateKolPromptParams{
|
||||
TenantID: input.TenantID,
|
||||
@@ -127,62 +215,56 @@ func (r *kolPromptRepository) Create(ctx context.Context, input CreateKolPromptI
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &KolPrompt{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
PackageID: row.PackageID,
|
||||
Name: row.Name,
|
||||
PlatformHint: nullableText(row.PlatformHint),
|
||||
Status: row.Status,
|
||||
SortOrder: int(row.SortOrder),
|
||||
PublishedRevisionNo: nullableIntFromInt4(row.PublishedRevisionNo),
|
||||
DraftRevisionNo: nullableIntFromInt4(row.DraftRevisionNo),
|
||||
CreatedBy: row.CreatedBy,
|
||||
UpdatedBy: row.UpdatedBy,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
}, nil
|
||||
return r.GetByID(ctx, input.TenantID, row.ID)
|
||||
}
|
||||
|
||||
func (r *kolPromptRepository) GetByID(ctx context.Context, tenantID, id int64) (*KolPrompt, error) {
|
||||
row, err := r.q.GetKolPromptByID(ctx, generated.GetKolPromptByIDParams{
|
||||
ID: id,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
row := r.db.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, package_id, name, platform_hint, status, sort_order,
|
||||
published_revision_no, draft_revision_no, prompt_asset_key, schema_json, card_config_json,
|
||||
created_by, updated_by, created_at, updated_at
|
||||
FROM kol_prompts
|
||||
WHERE id = $1
|
||||
AND tenant_id = $2
|
||||
AND deleted_at IS NULL
|
||||
`, id, tenantID)
|
||||
|
||||
prompt, err := scanKolPrompt(row)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return mapKolPrompt(row), nil
|
||||
return prompt, nil
|
||||
}
|
||||
|
||||
func (r *kolPromptRepository) ListByPackage(ctx context.Context, tenantID, packageID int64) ([]KolPrompt, error) {
|
||||
rows, err := r.q.ListKolPromptsByPackage(ctx, generated.ListKolPromptsByPackageParams{
|
||||
PackageID: packageID,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
rows, err := r.db.Query(ctx, `
|
||||
SELECT id, tenant_id, package_id, name, platform_hint, status, sort_order,
|
||||
published_revision_no, draft_revision_no, prompt_asset_key, schema_json, card_config_json,
|
||||
created_by, updated_by, created_at, updated_at
|
||||
FROM kol_prompts
|
||||
WHERE package_id = $1
|
||||
AND tenant_id = $2
|
||||
AND deleted_at IS NULL
|
||||
ORDER BY sort_order ASC, created_at ASC
|
||||
`, packageID, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make([]KolPrompt, len(rows))
|
||||
for i, row := range rows {
|
||||
result[i] = KolPrompt{
|
||||
ID: row.ID,
|
||||
TenantID: row.TenantID,
|
||||
PackageID: row.PackageID,
|
||||
Name: row.Name,
|
||||
PlatformHint: nullableText(row.PlatformHint),
|
||||
Status: row.Status,
|
||||
SortOrder: int(row.SortOrder),
|
||||
PublishedRevisionNo: nullableIntFromInt4(row.PublishedRevisionNo),
|
||||
DraftRevisionNo: nullableIntFromInt4(row.DraftRevisionNo),
|
||||
CreatedBy: row.CreatedBy,
|
||||
UpdatedBy: row.UpdatedBy,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
UpdatedAt: timeFromTimestamp(row.UpdatedAt),
|
||||
defer rows.Close()
|
||||
|
||||
result := make([]KolPrompt, 0)
|
||||
for rows.Next() {
|
||||
prompt, err := scanKolPrompt(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, *prompt)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -218,13 +300,50 @@ func (r *kolPromptRepository) UpdateMetadata(ctx context.Context, input UpdateKo
|
||||
})
|
||||
}
|
||||
|
||||
func (r *kolPromptRepository) SetDraftRevision(ctx context.Context, tenantID, promptID int64, revNo int, updatedBy int64) error {
|
||||
return r.q.UpdateKolPromptDraftRevision(ctx, generated.UpdateKolPromptDraftRevisionParams{
|
||||
RevisionNo: pgtype.Int4{Int32: int32(revNo), Valid: true},
|
||||
UpdatedBy: updatedBy,
|
||||
ID: promptID,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
func (r *kolPromptRepository) Save(ctx context.Context, input SaveKolPromptInput) error {
|
||||
publishedRevisionNo := pgtype.Int4{}
|
||||
if input.PublishedRevisionNo != nil {
|
||||
publishedRevisionNo = pgtype.Int4{Int32: int32(*input.PublishedRevisionNo), Valid: true}
|
||||
}
|
||||
|
||||
_, err := r.db.Exec(ctx, `
|
||||
UPDATE kol_prompts
|
||||
SET name = $1,
|
||||
platform_hint = $2,
|
||||
sort_order = $3,
|
||||
prompt_asset_key = $4,
|
||||
schema_json = $5::jsonb,
|
||||
card_config_json = $6::jsonb,
|
||||
status = $7,
|
||||
published_revision_no = $8,
|
||||
updated_by = $9,
|
||||
updated_at = NOW()
|
||||
WHERE id = $10
|
||||
AND tenant_id = $11
|
||||
AND deleted_at IS NULL
|
||||
`, input.Name, pgText(input.PlatformHint), int32(input.SortOrder), input.PromptAssetKey, input.SchemaJSON, input.CardConfigJSON, input.Status, publishedRevisionNo, input.UpdatedBy, input.ID, input.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *kolPromptRepository) Publish(ctx context.Context, input PublishKolPromptInput) error {
|
||||
_, err := r.db.Exec(ctx, `
|
||||
UPDATE kol_prompts
|
||||
SET name = $1,
|
||||
platform_hint = $2,
|
||||
sort_order = $3,
|
||||
prompt_asset_key = $4,
|
||||
schema_json = $5::jsonb,
|
||||
card_config_json = $6::jsonb,
|
||||
published_revision_no = $7,
|
||||
draft_revision_no = NULL,
|
||||
status = 'active',
|
||||
updated_by = $8,
|
||||
updated_at = NOW()
|
||||
WHERE id = $9
|
||||
AND tenant_id = $10
|
||||
AND deleted_at IS NULL
|
||||
`, input.Name, pgText(input.PlatformHint), int32(input.SortOrder), input.PromptAssetKey, input.SchemaJSON, input.CardConfigJSON, input.PublishedRevisionNo, input.UpdatedBy, input.ID, input.TenantID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *kolPromptRepository) SetPublishedRevision(ctx context.Context, tenantID, promptID int64, revNo int, updatedBy int64) error {
|
||||
@@ -252,68 +371,6 @@ func (r *kolPromptRepository) SoftDelete(ctx context.Context, tenantID, id int64
|
||||
})
|
||||
}
|
||||
|
||||
func (r *kolPromptRepository) CreateRevision(ctx context.Context, input CreateKolPromptRevisionInput) (*KolPromptRevision, error) {
|
||||
row, err := r.q.CreateKolPromptRevision(ctx, generated.CreateKolPromptRevisionParams{
|
||||
TenantID: input.TenantID,
|
||||
PromptID: input.PromptID,
|
||||
RevisionNo: int32(input.RevisionNo),
|
||||
PromptAssetKey: input.PromptAssetKey,
|
||||
SchemaJson: input.SchemaJSON,
|
||||
CardConfigJson: input.CardConfigJSON,
|
||||
CreatedBy: input.CreatedBy,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &KolPromptRevision{
|
||||
ID: row.ID,
|
||||
TenantID: input.TenantID,
|
||||
PromptID: row.PromptID,
|
||||
RevisionNo: int(row.RevisionNo),
|
||||
PromptAssetKey: row.PromptAssetKey,
|
||||
SchemaJSON: row.SchemaJson,
|
||||
CardConfigJSON: row.CardConfigJson,
|
||||
CreatedBy: row.CreatedBy,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *kolPromptRepository) GetRevision(ctx context.Context, tenantID, promptID int64, revNo int) (*KolPromptRevision, error) {
|
||||
row, err := r.q.GetKolPromptRevision(ctx, generated.GetKolPromptRevisionParams{
|
||||
PromptID: promptID,
|
||||
RevisionNo: int32(revNo),
|
||||
TenantID: tenantID,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &KolPromptRevision{
|
||||
ID: row.ID,
|
||||
TenantID: tenantID,
|
||||
PromptID: row.PromptID,
|
||||
RevisionNo: int(row.RevisionNo),
|
||||
PromptAssetKey: row.PromptAssetKey,
|
||||
SchemaJSON: row.SchemaJson,
|
||||
CardConfigJSON: row.CardConfigJson,
|
||||
CreatedBy: row.CreatedBy,
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *kolPromptRepository) NextRevisionNo(ctx context.Context, tenantID, promptID int64) (int, error) {
|
||||
next, err := r.q.NextKolPromptRevisionNo(ctx, generated.NextKolPromptRevisionNoParams{
|
||||
PromptID: promptID,
|
||||
TenantID: tenantID,
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(next), nil
|
||||
}
|
||||
|
||||
func nullableIntFromInt4(value pgtype.Int4) *int {
|
||||
if !value.Valid {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user