1075 lines
29 KiB
PHP
1075 lines
29 KiB
PHP
<?php
|
|
/*
|
|
* promptstore.php
|
|
*
|
|
* Prompt storage for Racket sandbox.
|
|
*
|
|
* Supports:
|
|
* - personal user prompts
|
|
* - global default prompts managed by admins
|
|
* - prompt versions
|
|
* - automatic SQLite migration
|
|
*
|
|
* Database:
|
|
* data/racket-sandbox.sqlite
|
|
*
|
|
* Model:
|
|
* user_id = 0 and is_default = 1:
|
|
* global default prompt
|
|
*
|
|
* user_id > 0 and is_default = 0:
|
|
* personal user prompt
|
|
*/
|
|
|
|
class PromptStoreException extends Exception
|
|
{
|
|
}
|
|
|
|
class PromptRecord
|
|
{
|
|
private $row;
|
|
|
|
public function __construct($row)
|
|
{
|
|
$this->row = is_array($row) ? $row : array();
|
|
}
|
|
|
|
public function id()
|
|
{
|
|
return (int)($this->row['id'] ?? 0);
|
|
}
|
|
|
|
public function userId()
|
|
{
|
|
return (int)($this->row['user_id'] ?? 0);
|
|
}
|
|
|
|
public function isDefault()
|
|
{
|
|
return (int)($this->row['is_default'] ?? 0) === 1;
|
|
}
|
|
|
|
public function defaultKey()
|
|
{
|
|
return (string)($this->row['default_key'] ?? '');
|
|
}
|
|
|
|
public function name()
|
|
{
|
|
return (string)($this->row['name'] ?? '');
|
|
}
|
|
|
|
public function language()
|
|
{
|
|
return (string)($this->row['language'] ?? '');
|
|
}
|
|
|
|
public function content()
|
|
{
|
|
return (string)($this->row['content'] ?? '');
|
|
}
|
|
|
|
public function createdAt()
|
|
{
|
|
return isset($this->row['created_at']) ? (int)$this->row['created_at'] : null;
|
|
}
|
|
|
|
public function updatedAt()
|
|
{
|
|
return isset($this->row['updated_at']) ? (int)$this->row['updated_at'] : null;
|
|
}
|
|
|
|
public function deletedAt()
|
|
{
|
|
return isset($this->row['deleted_at']) && $this->row['deleted_at'] !== null
|
|
? (int)$this->row['deleted_at']
|
|
: null;
|
|
}
|
|
|
|
public function raw()
|
|
{
|
|
return $this->row;
|
|
}
|
|
}
|
|
|
|
class PromptVersionRecord
|
|
{
|
|
private $row;
|
|
|
|
public function __construct($row)
|
|
{
|
|
$this->row = is_array($row) ? $row : array();
|
|
}
|
|
|
|
public function id()
|
|
{
|
|
return (int)($this->row['id'] ?? 0);
|
|
}
|
|
|
|
public function promptId()
|
|
{
|
|
return (int)($this->row['prompt_id'] ?? 0);
|
|
}
|
|
|
|
public function userId()
|
|
{
|
|
return (int)($this->row['user_id'] ?? 0);
|
|
}
|
|
|
|
public function versionNo()
|
|
{
|
|
return (int)($this->row['version_no'] ?? 0);
|
|
}
|
|
|
|
public function name()
|
|
{
|
|
return (string)($this->row['name'] ?? '');
|
|
}
|
|
|
|
public function language()
|
|
{
|
|
return (string)($this->row['language'] ?? '');
|
|
}
|
|
|
|
public function content()
|
|
{
|
|
return (string)($this->row['content'] ?? '');
|
|
}
|
|
|
|
public function note()
|
|
{
|
|
return (string)($this->row['note'] ?? '');
|
|
}
|
|
|
|
public function createdAt()
|
|
{
|
|
return isset($this->row['created_at']) ? (int)$this->row['created_at'] : null;
|
|
}
|
|
|
|
public function raw()
|
|
{
|
|
return $this->row;
|
|
}
|
|
}
|
|
|
|
class PromptStore
|
|
{
|
|
private $db;
|
|
|
|
public function __construct($dbFile)
|
|
{
|
|
$dir = dirname($dbFile);
|
|
|
|
if (!is_dir($dir)) {
|
|
if (!mkdir($dir, 0700, true)) {
|
|
throw new PromptStoreException('Could not create database directory: ' . $dir);
|
|
}
|
|
}
|
|
|
|
$this->db = new PDO('sqlite:' . $dbFile);
|
|
$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
$this->db->exec('PRAGMA journal_mode = WAL');
|
|
$this->db->exec('PRAGMA busy_timeout = 5000');
|
|
|
|
$this->init();
|
|
}
|
|
|
|
private function init()
|
|
{
|
|
/*
|
|
* Canonical schema.
|
|
*
|
|
* Existing databases are migrated below. In particular, this version
|
|
* does not require nullable user_id. Global defaults use user_id = 0.
|
|
*/
|
|
$this->db->exec(
|
|
'CREATE TABLE IF NOT EXISTS prompts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL DEFAULT 0,
|
|
is_default INTEGER NOT NULL DEFAULT 0,
|
|
default_key TEXT NULL,
|
|
name TEXT NOT NULL,
|
|
language TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL,
|
|
deleted_at INTEGER NULL
|
|
)'
|
|
);
|
|
|
|
$this->migratePromptsTable();
|
|
|
|
$this->db->exec(
|
|
'CREATE INDEX IF NOT EXISTS idx_prompts_user
|
|
ON prompts(user_id, is_default, deleted_at, language, name)'
|
|
);
|
|
|
|
$this->db->exec(
|
|
'CREATE INDEX IF NOT EXISTS idx_prompts_default
|
|
ON prompts(is_default, deleted_at, language, default_key)'
|
|
);
|
|
|
|
$this->db->exec(
|
|
'CREATE TABLE IF NOT EXISTS prompt_versions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
prompt_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL DEFAULT 0,
|
|
version_no INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
language TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
note TEXT NOT NULL DEFAULT "",
|
|
created_at INTEGER NOT NULL,
|
|
FOREIGN KEY(prompt_id) REFERENCES prompts(id),
|
|
UNIQUE(prompt_id, version_no)
|
|
)'
|
|
);
|
|
|
|
$this->migratePromptVersionsTable();
|
|
|
|
$this->db->exec(
|
|
'CREATE INDEX IF NOT EXISTS idx_prompt_versions_prompt
|
|
ON prompt_versions(prompt_id, version_no)'
|
|
);
|
|
}
|
|
|
|
private function migratePromptsTable()
|
|
{
|
|
$cols = $this->tableColumns('prompts');
|
|
|
|
if (!isset($cols['user_id'])) {
|
|
$this->db->exec('ALTER TABLE prompts ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0');
|
|
}
|
|
|
|
if (!isset($cols['is_default'])) {
|
|
$this->db->exec('ALTER TABLE prompts ADD COLUMN is_default INTEGER NOT NULL DEFAULT 0');
|
|
}
|
|
|
|
if (!isset($cols['default_key'])) {
|
|
$this->db->exec('ALTER TABLE prompts ADD COLUMN default_key TEXT NULL');
|
|
}
|
|
|
|
if (!isset($cols['deleted_at'])) {
|
|
$this->db->exec('ALTER TABLE prompts ADD COLUMN deleted_at INTEGER NULL');
|
|
}
|
|
|
|
/*
|
|
* Normalize existing rows.
|
|
*/
|
|
$this->db->exec(
|
|
'UPDATE prompts
|
|
SET is_default = 0
|
|
WHERE is_default IS NULL'
|
|
);
|
|
|
|
$this->db->exec(
|
|
'UPDATE prompts
|
|
SET user_id = 0
|
|
WHERE is_default = 1'
|
|
);
|
|
}
|
|
|
|
private function migratePromptVersionsTable()
|
|
{
|
|
$cols = $this->tableColumns('prompt_versions');
|
|
|
|
if (!isset($cols['user_id'])) {
|
|
$this->db->exec('ALTER TABLE prompt_versions ADD COLUMN user_id INTEGER NOT NULL DEFAULT 0');
|
|
}
|
|
|
|
if (!isset($cols['note'])) {
|
|
$this->db->exec('ALTER TABLE prompt_versions ADD COLUMN note TEXT NOT NULL DEFAULT ""');
|
|
}
|
|
|
|
$this->db->exec(
|
|
'UPDATE prompt_versions
|
|
SET user_id = 0
|
|
WHERE user_id IS NULL'
|
|
);
|
|
}
|
|
|
|
private function tableColumns($table)
|
|
{
|
|
$stmt = $this->db->query('PRAGMA table_info(' . $table . ')');
|
|
$cols = array();
|
|
|
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
$cols[$row['name']] = true;
|
|
}
|
|
|
|
return $cols;
|
|
}
|
|
|
|
public function supportedLanguages()
|
|
{
|
|
return array('en', 'nl');
|
|
}
|
|
|
|
public function languageLabel($language)
|
|
{
|
|
if ($language === 'nl') {
|
|
return 'Nederlands';
|
|
}
|
|
|
|
if ($language === 'en') {
|
|
return 'English';
|
|
}
|
|
|
|
return $language;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Personal prompts */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
public function listPrompts($userId, $language = null)
|
|
{
|
|
$userId = $this->safeUserId($userId);
|
|
|
|
if ($language !== null && $language !== '') {
|
|
$language = $this->safeLanguage($language);
|
|
|
|
$stmt = $this->db->prepare(
|
|
'SELECT *
|
|
FROM prompts
|
|
WHERE user_id = :user_id
|
|
AND is_default = 0
|
|
AND deleted_at IS NULL
|
|
AND language = :language
|
|
ORDER BY name COLLATE NOCASE'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':user_id' => $userId,
|
|
':language' => $language,
|
|
));
|
|
} else {
|
|
$stmt = $this->db->prepare(
|
|
'SELECT *
|
|
FROM prompts
|
|
WHERE user_id = :user_id
|
|
AND is_default = 0
|
|
AND deleted_at IS NULL
|
|
ORDER BY language, name COLLATE NOCASE'
|
|
);
|
|
|
|
$stmt->execute(array(':user_id' => $userId));
|
|
}
|
|
|
|
return $this->promptRows($stmt);
|
|
}
|
|
|
|
public function getPrompt($userId, $promptId)
|
|
{
|
|
$userId = $this->safeUserId($userId);
|
|
$promptId = $this->safeId($promptId);
|
|
|
|
$stmt = $this->db->prepare(
|
|
'SELECT *
|
|
FROM prompts
|
|
WHERE id = :id
|
|
AND user_id = :user_id
|
|
AND is_default = 0
|
|
AND deleted_at IS NULL'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':id' => $promptId,
|
|
':user_id' => $userId,
|
|
));
|
|
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
return $row ? new PromptRecord($row) : null;
|
|
}
|
|
|
|
public function createPrompt($userId, $name, $language, $content)
|
|
{
|
|
$userId = $this->safeUserId($userId);
|
|
$name = $this->safeName($name);
|
|
$language = $this->safeLanguage($language);
|
|
$content = $this->safeContent($content);
|
|
|
|
$now = time();
|
|
|
|
$stmt = $this->db->prepare(
|
|
'INSERT INTO prompts
|
|
(user_id, is_default, default_key, name, language, content,
|
|
created_at, updated_at, deleted_at)
|
|
VALUES
|
|
(:user_id, 0, NULL, :name, :language, :content,
|
|
:created_at, :updated_at, NULL)'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':user_id' => $userId,
|
|
':name' => $name,
|
|
':language' => $language,
|
|
':content' => $content,
|
|
':created_at' => $now,
|
|
':updated_at' => $now,
|
|
));
|
|
|
|
$promptId = (int)$this->db->lastInsertId();
|
|
$this->createVersionForPrompt($promptId, $userId, 'initial version');
|
|
|
|
return $this->getPrompt($userId, $promptId);
|
|
}
|
|
|
|
public function updatePrompt($userId, $promptId, $name, $language, $content, $createVersion, $note = '')
|
|
{
|
|
$userId = $this->safeUserId($userId);
|
|
$promptId = $this->safeId($promptId);
|
|
$name = $this->safeName($name);
|
|
$language = $this->safeLanguage($language);
|
|
$content = $this->safeContent($content);
|
|
|
|
$prompt = $this->getPrompt($userId, $promptId);
|
|
|
|
if ($prompt === null) {
|
|
throw new PromptStoreException('Prompt not found.');
|
|
}
|
|
|
|
$stmt = $this->db->prepare(
|
|
'UPDATE prompts
|
|
SET name = :name,
|
|
language = :language,
|
|
content = :content,
|
|
updated_at = :updated_at
|
|
WHERE id = :id
|
|
AND user_id = :user_id
|
|
AND is_default = 0
|
|
AND deleted_at IS NULL'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':name' => $name,
|
|
':language' => $language,
|
|
':content' => $content,
|
|
':updated_at' => time(),
|
|
':id' => $promptId,
|
|
':user_id' => $userId,
|
|
));
|
|
|
|
if ($createVersion) {
|
|
$this->createVersionForPrompt($promptId, $userId, $note);
|
|
}
|
|
|
|
return $this->getPrompt($userId, $promptId);
|
|
}
|
|
|
|
public function deletePrompt($userId, $promptId)
|
|
{
|
|
$userId = $this->safeUserId($userId);
|
|
$promptId = $this->safeId($promptId);
|
|
|
|
$stmt = $this->db->prepare(
|
|
'UPDATE prompts
|
|
SET deleted_at = :deleted_at,
|
|
updated_at = :updated_at
|
|
WHERE id = :id
|
|
AND user_id = :user_id
|
|
AND is_default = 0
|
|
AND deleted_at IS NULL'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':deleted_at' => time(),
|
|
':updated_at' => time(),
|
|
':id' => $promptId,
|
|
':user_id' => $userId,
|
|
));
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Default prompts */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
public function listDefaultPrompts($language = null)
|
|
{
|
|
if ($language !== null && $language !== '') {
|
|
$language = $this->safeLanguage($language);
|
|
|
|
$stmt = $this->db->prepare(
|
|
'SELECT *
|
|
FROM prompts
|
|
WHERE user_id = 0
|
|
AND is_default = 1
|
|
AND deleted_at IS NULL
|
|
AND language = :language
|
|
ORDER BY name COLLATE NOCASE'
|
|
);
|
|
|
|
$stmt->execute(array(':language' => $language));
|
|
} else {
|
|
$stmt = $this->db->query(
|
|
'SELECT *
|
|
FROM prompts
|
|
WHERE user_id = 0
|
|
AND is_default = 1
|
|
AND deleted_at IS NULL
|
|
ORDER BY language, name COLLATE NOCASE'
|
|
);
|
|
}
|
|
|
|
return $this->promptRows($stmt);
|
|
}
|
|
|
|
public function getDefaultPrompt($promptId)
|
|
{
|
|
$promptId = $this->safeId($promptId);
|
|
|
|
$stmt = $this->db->prepare(
|
|
'SELECT *
|
|
FROM prompts
|
|
WHERE id = :id
|
|
AND user_id = 0
|
|
AND is_default = 1
|
|
AND deleted_at IS NULL'
|
|
);
|
|
|
|
$stmt->execute(array(':id' => $promptId));
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
return $row ? new PromptRecord($row) : null;
|
|
}
|
|
|
|
public function createDefaultPrompt($defaultKey, $name, $language, $content)
|
|
{
|
|
$defaultKey = $this->safeDefaultKey($defaultKey);
|
|
$name = $this->safeName($name);
|
|
$language = $this->safeLanguage($language);
|
|
$content = $this->safeContent($content);
|
|
|
|
$now = time();
|
|
|
|
$stmt = $this->db->prepare(
|
|
'INSERT INTO prompts
|
|
(user_id, is_default, default_key, name, language, content,
|
|
created_at, updated_at, deleted_at)
|
|
VALUES
|
|
(0, 1, :default_key, :name, :language, :content,
|
|
:created_at, :updated_at, NULL)'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':default_key' => $defaultKey,
|
|
':name' => $name,
|
|
':language' => $language,
|
|
':content' => $content,
|
|
':created_at' => $now,
|
|
':updated_at' => $now,
|
|
));
|
|
|
|
$promptId = (int)$this->db->lastInsertId();
|
|
$this->createVersionForPrompt($promptId, 0, 'initial default version');
|
|
|
|
return $this->getDefaultPrompt($promptId);
|
|
}
|
|
|
|
public function updateDefaultPrompt($promptId, $defaultKey, $name, $language, $content, $createVersion, $note = '')
|
|
{
|
|
$promptId = $this->safeId($promptId);
|
|
$defaultKey = $this->safeDefaultKey($defaultKey);
|
|
$name = $this->safeName($name);
|
|
$language = $this->safeLanguage($language);
|
|
$content = $this->safeContent($content);
|
|
|
|
$prompt = $this->getDefaultPrompt($promptId);
|
|
|
|
if ($prompt === null) {
|
|
throw new PromptStoreException('Default prompt not found.');
|
|
}
|
|
|
|
$stmt = $this->db->prepare(
|
|
'UPDATE prompts
|
|
SET default_key = :default_key,
|
|
name = :name,
|
|
language = :language,
|
|
content = :content,
|
|
updated_at = :updated_at
|
|
WHERE id = :id
|
|
AND user_id = 0
|
|
AND is_default = 1
|
|
AND deleted_at IS NULL'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':default_key' => $defaultKey,
|
|
':name' => $name,
|
|
':language' => $language,
|
|
':content' => $content,
|
|
':updated_at' => time(),
|
|
':id' => $promptId,
|
|
));
|
|
|
|
if ($createVersion) {
|
|
$this->createVersionForPrompt($promptId, 0, $note);
|
|
}
|
|
|
|
return $this->getDefaultPrompt($promptId);
|
|
}
|
|
|
|
public function deleteDefaultPrompt($promptId)
|
|
{
|
|
$promptId = $this->safeId($promptId);
|
|
|
|
$stmt = $this->db->prepare(
|
|
'UPDATE prompts
|
|
SET deleted_at = :deleted_at,
|
|
updated_at = :updated_at
|
|
WHERE id = :id
|
|
AND user_id = 0
|
|
AND is_default = 1
|
|
AND deleted_at IS NULL'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':deleted_at' => time(),
|
|
':updated_at' => time(),
|
|
':id' => $promptId,
|
|
));
|
|
}
|
|
|
|
public function copyDefaultPromptToUser($userId, $defaultPromptId)
|
|
{
|
|
$userId = $this->safeUserId($userId);
|
|
$defaultPromptId = $this->safeId($defaultPromptId);
|
|
|
|
$default = $this->getDefaultPrompt($defaultPromptId);
|
|
|
|
if ($default === null) {
|
|
throw new PromptStoreException('Default prompt not found.');
|
|
}
|
|
|
|
$now = time();
|
|
|
|
$stmt = $this->db->prepare(
|
|
'INSERT INTO prompts
|
|
(user_id, is_default, default_key, name, language, content,
|
|
created_at, updated_at, deleted_at)
|
|
VALUES
|
|
(:user_id, 0, :default_key, :name, :language, :content,
|
|
:created_at, :updated_at, NULL)'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':user_id' => $userId,
|
|
':default_key' => $default->defaultKey(),
|
|
':name' => $default->name(),
|
|
':language' => $default->language(),
|
|
':content' => $default->content(),
|
|
':created_at' => $now,
|
|
':updated_at' => $now,
|
|
));
|
|
|
|
$promptId = (int)$this->db->lastInsertId();
|
|
$this->createVersionForPrompt($promptId, $userId, 'copied from default ' . $default->defaultKey());
|
|
|
|
return $this->getPrompt($userId, $promptId);
|
|
}
|
|
|
|
public function copyAllDefaultsToUser($userId, $language)
|
|
{
|
|
$userId = $this->safeUserId($userId);
|
|
$language = $this->safeLanguage($language);
|
|
|
|
$defaults = $this->listDefaultPrompts($language);
|
|
$created = 0;
|
|
|
|
foreach ($defaults as $default) {
|
|
if ($this->userHasPromptForDefault($userId, $default->defaultKey(), $language)) {
|
|
continue;
|
|
}
|
|
|
|
$this->copyDefaultPromptToUser($userId, $default->id());
|
|
$created++;
|
|
}
|
|
|
|
return $created;
|
|
}
|
|
|
|
public function userHasPromptForDefault($userId, $defaultKey, $language)
|
|
{
|
|
$userId = $this->safeUserId($userId);
|
|
$defaultKey = $this->safeDefaultKey($defaultKey);
|
|
$language = $this->safeLanguage($language);
|
|
|
|
$stmt = $this->db->prepare(
|
|
'SELECT id
|
|
FROM prompts
|
|
WHERE user_id = :user_id
|
|
AND is_default = 0
|
|
AND deleted_at IS NULL
|
|
AND default_key = :default_key
|
|
AND language = :language'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':user_id' => $userId,
|
|
':default_key' => $defaultKey,
|
|
':language' => $language,
|
|
));
|
|
|
|
return $stmt->fetch(PDO::FETCH_ASSOC) ? true : false;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Versions */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
public function createVersion($userId, $promptId, $note = '')
|
|
{
|
|
$userId = $this->safeUserId($userId);
|
|
$promptId = $this->safeId($promptId);
|
|
|
|
if ($this->getPrompt($userId, $promptId) === null) {
|
|
throw new PromptStoreException('Prompt not found.');
|
|
}
|
|
|
|
return $this->createVersionForPrompt($promptId, $userId, $note);
|
|
}
|
|
|
|
public function createDefaultVersion($promptId, $note = '')
|
|
{
|
|
$promptId = $this->safeId($promptId);
|
|
|
|
if ($this->getDefaultPrompt($promptId) === null) {
|
|
throw new PromptStoreException('Default prompt not found.');
|
|
}
|
|
|
|
return $this->createVersionForPrompt($promptId, 0, $note);
|
|
}
|
|
|
|
public function listVersions($userId, $promptId)
|
|
{
|
|
$userId = $this->safeUserId($userId);
|
|
$promptId = $this->safeId($promptId);
|
|
|
|
if ($this->getPrompt($userId, $promptId) === null) {
|
|
throw new PromptStoreException('Prompt not found.');
|
|
}
|
|
|
|
return $this->listVersionsForPrompt($promptId);
|
|
}
|
|
|
|
public function listDefaultVersions($promptId)
|
|
{
|
|
$promptId = $this->safeId($promptId);
|
|
|
|
if ($this->getDefaultPrompt($promptId) === null) {
|
|
throw new PromptStoreException('Default prompt not found.');
|
|
}
|
|
|
|
return $this->listVersionsForPrompt($promptId);
|
|
}
|
|
|
|
public function restoreVersion($userId, $promptId, $versionNo)
|
|
{
|
|
$userId = $this->safeUserId($userId);
|
|
$promptId = $this->safeId($promptId);
|
|
|
|
if ($this->getPrompt($userId, $promptId) === null) {
|
|
throw new PromptStoreException('Prompt not found.');
|
|
}
|
|
|
|
$this->restoreVersionForPrompt($promptId, $versionNo, $userId);
|
|
}
|
|
|
|
public function restoreDefaultVersion($promptId, $versionNo)
|
|
{
|
|
$promptId = $this->safeId($promptId);
|
|
|
|
if ($this->getDefaultPrompt($promptId) === null) {
|
|
throw new PromptStoreException('Default prompt not found.');
|
|
}
|
|
|
|
$this->restoreVersionForPrompt($promptId, $versionNo, 0);
|
|
}
|
|
|
|
private function createVersionForPrompt($promptId, $userId, $note = '')
|
|
{
|
|
$promptId = $this->safeId($promptId);
|
|
$userId = (int)$userId;
|
|
$note = trim((string)$note);
|
|
|
|
if (strlen($note) > 500) {
|
|
$note = substr($note, 0, 500);
|
|
}
|
|
|
|
$stmt = $this->db->prepare(
|
|
'SELECT *
|
|
FROM prompts
|
|
WHERE id = :id
|
|
AND deleted_at IS NULL'
|
|
);
|
|
|
|
$stmt->execute(array(':id' => $promptId));
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$row) {
|
|
throw new PromptStoreException('Prompt not found.');
|
|
}
|
|
|
|
$stmt = $this->db->prepare(
|
|
'SELECT COALESCE(MAX(version_no), 0) + 1 AS next_no
|
|
FROM prompt_versions
|
|
WHERE prompt_id = :prompt_id'
|
|
);
|
|
|
|
$stmt->execute(array(':prompt_id' => $promptId));
|
|
$versionRow = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
$versionNo = (int)$versionRow['next_no'];
|
|
|
|
$stmt = $this->db->prepare(
|
|
'INSERT INTO prompt_versions
|
|
(prompt_id, user_id, version_no, name, language, content, note, created_at)
|
|
VALUES
|
|
(:prompt_id, :user_id, :version_no, :name, :language, :content, :note, :created_at)'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':prompt_id' => $promptId,
|
|
':user_id' => $userId,
|
|
':version_no' => $versionNo,
|
|
':name' => $row['name'],
|
|
':language' => $row['language'],
|
|
':content' => $row['content'],
|
|
':note' => $note,
|
|
':created_at' => time(),
|
|
));
|
|
|
|
return $versionNo;
|
|
}
|
|
|
|
private function listVersionsForPrompt($promptId)
|
|
{
|
|
$promptId = $this->safeId($promptId);
|
|
|
|
$stmt = $this->db->prepare(
|
|
'SELECT *
|
|
FROM prompt_versions
|
|
WHERE prompt_id = :prompt_id
|
|
ORDER BY version_no DESC'
|
|
);
|
|
|
|
$stmt->execute(array(':prompt_id' => $promptId));
|
|
|
|
$out = array();
|
|
|
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
$out[] = new PromptVersionRecord($row);
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
private function restoreVersionForPrompt($promptId, $versionNo, $userId)
|
|
{
|
|
$promptId = $this->safeId($promptId);
|
|
$versionNo = (int)$versionNo;
|
|
$userId = (int)$userId;
|
|
|
|
if ($versionNo <= 0) {
|
|
throw new PromptStoreException('Invalid version number.');
|
|
}
|
|
|
|
$stmt = $this->db->prepare(
|
|
'SELECT *
|
|
FROM prompt_versions
|
|
WHERE prompt_id = :prompt_id
|
|
AND version_no = :version_no'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':prompt_id' => $promptId,
|
|
':version_no' => $versionNo,
|
|
));
|
|
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$row) {
|
|
throw new PromptStoreException('Version not found.');
|
|
}
|
|
|
|
$stmt = $this->db->prepare(
|
|
'UPDATE prompts
|
|
SET name = :name,
|
|
language = :language,
|
|
content = :content,
|
|
updated_at = :updated_at
|
|
WHERE id = :id
|
|
AND deleted_at IS NULL'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':name' => $row['name'],
|
|
':language' => $row['language'],
|
|
':content' => $row['content'],
|
|
':updated_at' => time(),
|
|
':id' => $promptId,
|
|
));
|
|
|
|
$this->createVersionForPrompt($promptId, $userId, 'restored version ' . $versionNo);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
private function promptRows($stmt)
|
|
{
|
|
$out = array();
|
|
|
|
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
|
$out[] = new PromptRecord($row);
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
private function safeUserId($userId)
|
|
{
|
|
$userId = (int)$userId;
|
|
|
|
if ($userId <= 0) {
|
|
throw new PromptStoreException('Invalid user id.');
|
|
}
|
|
|
|
return $userId;
|
|
}
|
|
|
|
private function safeId($id)
|
|
{
|
|
$id = (int)$id;
|
|
|
|
if ($id <= 0) {
|
|
throw new PromptStoreException('Invalid id.');
|
|
}
|
|
|
|
return $id;
|
|
}
|
|
|
|
private function safeName($name)
|
|
{
|
|
$name = trim((string)$name);
|
|
|
|
if ($name === '') {
|
|
throw new PromptStoreException('Prompt name is required.');
|
|
}
|
|
|
|
if (strlen($name) > 160) {
|
|
throw new PromptStoreException('Prompt name is too long.');
|
|
}
|
|
|
|
return $name;
|
|
}
|
|
|
|
private function safeLanguage($language)
|
|
{
|
|
$language = strtolower(trim((string)$language));
|
|
|
|
if (!in_array($language, $this->supportedLanguages(), true)) {
|
|
throw new PromptStoreException('Unsupported language: ' . $language);
|
|
}
|
|
|
|
return $language;
|
|
}
|
|
|
|
private function safeContent($content)
|
|
{
|
|
$content = trim((string)$content);
|
|
|
|
if ($content === '') {
|
|
throw new PromptStoreException('Prompt content is required.');
|
|
}
|
|
|
|
if (strlen($content) > 200000) {
|
|
throw new PromptStoreException('Prompt content is too large.');
|
|
}
|
|
|
|
return $content;
|
|
}
|
|
|
|
private function safeDefaultKey($key)
|
|
{
|
|
$key = trim((string)$key);
|
|
|
|
if ($key === '') {
|
|
throw new PromptStoreException('Default key is required.');
|
|
}
|
|
|
|
if (!preg_match('/^[A-Za-z0-9_-]+$/', $key)) {
|
|
throw new PromptStoreException('Invalid default key.');
|
|
}
|
|
|
|
return $key;
|
|
}
|
|
|
|
public function deleteVersion($userId, $promptId, $versionNo)
|
|
{
|
|
$userId = $this->safeUserId($userId);
|
|
$promptId = $this->safeId($promptId);
|
|
$versionNo = (int)$versionNo;
|
|
|
|
if ($versionNo <= 0) {
|
|
throw new PromptStoreException('Invalid version number.');
|
|
}
|
|
|
|
if ($this->getPrompt($userId, $promptId) === null) {
|
|
throw new PromptStoreException('Prompt not found.');
|
|
}
|
|
|
|
$this->deleteVersionForPrompt($promptId, $versionNo);
|
|
}
|
|
|
|
public function deleteDefaultVersion($promptId, $versionNo)
|
|
{
|
|
$promptId = $this->safeId($promptId);
|
|
$versionNo = (int)$versionNo;
|
|
|
|
if ($versionNo <= 0) {
|
|
throw new PromptStoreException('Invalid version number.');
|
|
}
|
|
|
|
if ($this->getDefaultPrompt($promptId) === null) {
|
|
throw new PromptStoreException('Default prompt not found.');
|
|
}
|
|
|
|
$this->deleteVersionForPrompt($promptId, $versionNo);
|
|
}
|
|
|
|
private function deleteVersionForPrompt($promptId, $versionNo)
|
|
{
|
|
$promptId = $this->safeId($promptId);
|
|
$versionNo = (int)$versionNo;
|
|
|
|
$stmt = $this->db->prepare(
|
|
'SELECT COUNT(*) AS n
|
|
FROM prompt_versions
|
|
WHERE prompt_id = :prompt_id'
|
|
);
|
|
|
|
$stmt->execute(array(':prompt_id' => $promptId));
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$row || (int)$row['n'] <= 1) {
|
|
throw new PromptStoreException('Cannot delete the last remaining version.');
|
|
}
|
|
|
|
$stmt = $this->db->prepare(
|
|
'DELETE FROM prompt_versions
|
|
WHERE prompt_id = :prompt_id
|
|
AND version_no = :version_no'
|
|
);
|
|
|
|
$stmt->execute(array(
|
|
':prompt_id' => $promptId,
|
|
':version_no' => $versionNo,
|
|
));
|
|
|
|
if ($stmt->rowCount() !== 1) {
|
|
throw new PromptStoreException('Version not found.');
|
|
}
|
|
}
|
|
} |