docs: detailed comments; fix: var declarations, no DROP, text inputs

This commit is contained in:
2026-07-20 08:13:26 +04:00
parent 53e12ecddd
commit d0fb4df1cb
2 changed files with 169 additions and 23 deletions
+111 -15
View File
@@ -1,67 +1,163 @@
<!---
Application.cfc — конфигурация Lucee CRUD-приложения
Жизненный цикл:
1. OnApplicationStart — вызывается ОДИН раз при старте приложения
- читает TABLE_NAME из env vars (json_env в Terraform)
- создаёт таблицу CREATE TABLE IF NOT EXISTS
- сохраняет имя таблицы в application.tableName
2. OnRequestStart — вызывается ПЕРЕД каждым запросом
- копирует application.tableName → request.tableName
3. OnRequest — вызывает index.cfm с CRUD-логикой
Datasource: testds, конфигурируется из env vars с префиксом testds_
(testds_class, testds_connectionString, testds_username, testds_password, ...)
Terraform json_env должен содержать:
- TABLE_NAME — имя таблицы (по умолчанию check_point)
- testds_class — JDBC-драйвер
- testds_connectionString — строка подключения к PostgreSQL
- testds_username — имя пользователя БД
- testds_password — пароль пользователя БД
--->
<cfcomponent displayname="Application" output="true">
<!--- Идентификатор приложения для Lucee --->
<cfset this.Name = "tfluceecrud" />
<!--- Таймаут приложения: 3 часа бездействия → перезапуск (вызовет OnApplicationStart заново) --->
<cfset this.applicationTimeout = createTimeSpan(0, 0, 3, 0) />
<!--- Включаем сессии для защиты от CSRF (Lucee автоматически добавляет токены) --->
<cfset this.sessionmanagement = "Yes" />
<!--- Сессия живёт 2 часа неактивности --->
<cfset this.sessiontimeout = CreateTimeSpan(0, 0, 120, 0) />
<!--- Имя datasource по умолчанию — используется в cfquery без атрибута datasource --->
<cfset this.datasource = "testds" />
<!--- Конфигурируем datasource из env vars с префиксом testds_ --->
<cfset getDS(this.datasource, this.datasource) />
<!--- Отключаем отладку на проде, таймаут запроса 20 секунд --->
<cfsetting requesttimeout="20" showdebugoutput="false" />
<cffunction name="OnApplicationStart" access="public" returntype="boolean">
<cfreturn true />
</cffunction>
<cffunction name="OnRequestStart" access="public" returntype="boolean">
<cfset request.tableName = createObject("java", "java.lang.System").getEnv().get("TABLE_NAME") />
<cfif NOT isDefined("request.tableName") OR NOT len(request.tableName)>
<cfset request.tableName = "check_point" />
<!---
OnApplicationStart — выполняется ОДИН раз при старте приложения.
Здесь: создаём таблицу если её нет, сохраняем имя в application scope.
application scope — живёт пока приложение не перезапустится (таймаут 3 часа).
Используем application а не request потому что request умирает после каждого запроса.
--->
<cffunction name="OnApplicationStart" access="public" returntype="boolean">
<!--- 1. Читаем имя таблицы из переменной окружения TABLE_NAME --->
<cfset var tableName = createObject("java", "java.lang.System").getEnv().get("TABLE_NAME") />
<!--- 2. Если переменная не задана в json_env — используем значение по умолчанию --->
<cfif NOT isDefined("tableName") OR NOT len(tableName)>
<cfset tableName = "check_point" />
</cfif>
<!--- 3. Сохраняем в application scope — доступно всем запросам этого приложения --->
<cfset application.tableName = tableName />
<!--- 4. Создаём таблицу если она ещё не существует (IF NOT EXISTS) --->
<!--- id: автоинкрементный первичный ключ --->
<!--- value: текстовое поле для CRUD-значений --->
<cftry>
<cfquery>
DROP TABLE IF EXISTS #request.tableName#
</cfquery>
<cfquery>
CREATE TABLE #request.tableName# (
CREATE TABLE IF NOT EXISTS #tableName# (
id SERIAL PRIMARY KEY,
value TEXT NOT NULL DEFAULT ''
)
</cfquery>
<!--- Игнорируем ошибки: нет прав, таблица уже есть, БД недоступна --->
<cfcatch type="database"></cfcatch>
</cftry>
<cfreturn true />
</cffunction>
<!---
OnRequestStart — выполняется ПЕРЕД каждым HTTP-запросом.
Здесь: копируем имя таблицы из application в request scope.
request scope — живёт только в течение одного запроса.
Каждая страница (index.cfm) читает request.tableName.
--->
<cffunction name="OnRequestStart" access="public" returntype="boolean">
<!--- Если приложение уже стартовало — берём имя из application --->
<cfif isDefined("application.tableName")>
<cfset request.tableName = application.tableName />
<!--- Иначе (первый запрос до OnApplicationStart) — fallback --->
<cfelse>
<cfset request.tableName = "check_point" />
</cfif>
<cfreturn true />
</cffunction>
<!---
OnRequest — обработчик каждого запроса.
Включает целевой шаблон (index.cfm) с установленной кодировкой.
--->
<cffunction name="OnRequest" access="public" returntype="void">
<cfargument name="template" type="string" required="true" />
<!--- UTF-8 для форм и URL-параметров --->
<cfset setEncoding("FORM", "UTF-8") />
<cfset setEncoding("URL", "UTF-8") />
<!--- Вычисляем web-корень для ссылок --->
<cfset local.basePath = getDirectoryFromPath(getCurrentTemplatePath()) />
<cfset request.webRoot = "../" />
<cfset request.thisPage = Replace(ReplaceNoCase(expandPath(arguments.template), local.basePath, ""), "\", "/") />
<!--- Выполняем запрошенный шаблон (index.cfm) --->
<cfinclude template="#arguments.template#" />
</cffunction>
<cffunction name="getDS" access="private" returntype="void">
<cfargument name="dsname" type="string" required="true" />
<cfargument name="prefix" type="string" default="#dsname#" />
<cfset system = createObject("java", "java.lang.System") />
<!---
getDS — конфигурирует datasource Lucee из переменных окружения.
Соглашение об именах env vars:
{prefix}_{field} → параметр datasource
Пример для testds:
testds_class → JDBC-драйвер
testds_connectionString → jdbc:postgresql://host:5432/db
testds_username → пользователь
testds_password → пароль
Все параметры передаются через json_env в Terraform.
--->
<cffunction name="getDS" access="private" returntype="void">
<cfargument name="dsname" type="string" required="true" hint="Имя datasource (testds)" />
<cfargument name="prefix" type="string" default="#dsname#" hint="Префикс env vars" />
<!--- Java-объект для чтения переменных окружения --->
<cfset var system = createObject("java", "java.lang.System") />
<!--- Словарь параметров datasource --->
<cfset var ds = {} />
<!--- Перебираем все возможные поля datasource --->
<cfloop list="class,connectionString,username,password,bundleName,bundleVersion,connectionLimit,liveTimeout,validate" item="field">
<!--- Читаем env var: {prefix}_{field}, например testds_username --->
<cfset var value = system.getEnv("#arguments.prefix#_#field#") />
<!--- Если переменная задана и не пуста — добавляем в datasource --->
<cfif isDefined("value") AND len(value)>
<cfset structInsert(ds, field, value) />
</cfif>
</cfloop>
<!--- Регистрируем datasource в Lucee --->
<cfset THIS.datasources[dsname] = ds />
</cffunction>
+57 -7
View File
@@ -1,15 +1,41 @@
<!---
index.cfm — CRUD-интерфейс для таблицы в PostgreSQL
Логика:
1. Если POST-запрос с FORM.action — выполняем create/update/delete
и редиректим на GET (POST-redirect-GET паттерн)
2. Если GET-запрос с URL.edit — загружаем запись для редактирования
3. Всегда показываем список всех записей + форму добавления/редактирования
Безопасность:
- cfqueryparam для всех входных данных → защита от SQL-инъекций
- cflocation после POST → защита от повторной отправки формы
- cfoutput только где нужно → XSS-защита через автоматическое экранирование Lucee
Переменные:
- request.tableName — имя таблицы (из Application.cfc / json_env TABLE_NAME)
- FORM.action — create | update | delete (POST)
- FORM.value — значение записи (POST)
- FORM.id — ID записи для update/delete (POST)
- URL.edit — ID записи для редактирования (GET)
--->
<cfset tableName = request.tableName />
<!--- Обработка действий --->
<!--- ====== ОБРАБОТКА POST-ЗАПРОСОВ (create / update / delete) ====== --->
<!--- isDefined("FORM.action") — true только если страница вызвана через <form method="post"> --->
<cfif isDefined("FORM.action")>
<!--- CREATE: вставка новой записи --->
<cfif FORM.action EQ "create">
<!--- cfqueryparam cfsqltype="cf_sql_varchar" — экранирует значение, защита от SQL-инъекций --->
<cfquery name="insertRow">
INSERT INTO #tableName# (value)
VALUES (<cfqueryparam value="#FORM.value#" cfsqltype="cf_sql_varchar" />)
</cfquery>
</cfif>
<!--- UPDATE: обновление существующей записи по ID --->
<cfif FORM.action EQ "update">
<cfquery name="updateRow">
UPDATE #tableName#
@@ -18,6 +44,7 @@
</cfquery>
</cfif>
<!--- DELETE: удаление записи по ID --->
<cfif FORM.action EQ "delete">
<cfquery name="deleteRow">
DELETE FROM #tableName#
@@ -25,22 +52,30 @@
</cfquery>
</cfif>
<!--- POST-redirect-GET: после любого действия — редирект на чистый URL без POST-данных.
Это предотвращает повторную отправку формы при обновлении страницы (F5). --->
<cflocation url="index.cfm" addtoken="no" />
</cfif>
<!--- Режим редактирования --->
<!--- ====== РЕЖИМ РЕДАКТИРОВАНИЯ (GET-запрос с ?edit=ID) ====== --->
<!--- editRow всегда инициализируем пустым словарём.
Если URL.edit не задан или запись не найдена — editRow останется пустым. --->
<cfset editRow = {} />
<cfif isDefined("URL.edit")>
<!--- Загружаем запись по ID из URL --->
<cfquery name="editQuery">
SELECT id, value FROM #tableName#
WHERE id = <cfqueryparam value="#URL.edit#" cfsqltype="cf_sql_integer" />
</cfquery>
<!--- Если запись найдена — заполняем editRow данными из запроса --->
<cfif editQuery.recordCount GT 0>
<cfset editRow = { id = editQuery.id, value = editQuery.value } />
</cfif>
</cfif>
<!--- Все записи --->
<!--- ====== СПИСОК ВСЕХ ЗАПИСЕЙ ====== --->
<cfquery name="rows">
SELECT id, value FROM #tableName# ORDER BY id
</cfquery>
@@ -64,18 +99,23 @@
<h1>#tableName#</h1>
<!--- Таблица записей --->
<table>
<tr>
<th>ID</th>
<th>Value</th>
<th></th>
</tr>
<!--- cfoutput query="rows" — цикл по всем строкам результата SELECT --->
<cfoutput query="rows">
<tr>
<td>#id#</td>
<td>#value#</td>
<td class="actions">
<!--- Ссылка на редактирование: index.cfm?edit={id} --->
<a href="index.cfm?edit=#id#">✏️</a>
<!--- Форма удаления: POST с action=delete и id записи.
JS confirm() — защита от случайного удаления. --->
<form method="post" style="display:inline">
<input type="hidden" name="action" value="delete" />
<input type="hidden" name="id" value="#id#" />
@@ -84,28 +124,38 @@
</td>
</tr>
</cfoutput>
<!--- Если записей нет — показываем информационное сообщение --->
<cfif rows.recordCount EQ 0>
<tr><td colspan="3">Нет записей. Создайте первую.</td></tr>
</cfif>
</table>
<cfif NOT structIsEmpty(editRow)><cfoutput>
<!--- ====== ФОРМА: редактирование ИЛИ добавление ====== --->
<!--- structIsEmpty(editRow) — true если нет записи для редактирования → показываем форму ДОБАВЛЕНИЯ --->
<!--- false если URL.edit задан и запись найдена → показываем форму РЕДАКТИРОВАНИЯ --->
<cfif NOT structIsEmpty(editRow)>
<!--- cfoutput нужен чтобы #editRow.id# и #editRow.value# вычислились --->
<cfoutput>
<h2>Редактировать #editRow.id#</h2>
<form method="post">
<input type="hidden" name="action" value="update" />
<input type="hidden" name="id" value="#editRow.id#" />
<input type="number" name="value" value="#editRow.value#" required />
<!--- type="text" — позволяет вводить любые символы, не только цифры --->
<input type="text" name="value" value="#editRow.value#" required />
<button type="submit">Сохранить</button>
<a href="index.cfm">Отмена</a>
</form></cfoutput>
</form>
</cfoutput>
<cfelse>
<h2>Добавить</h2>
<form method="post">
<input type="hidden" name="action" value="create" />
<input type="number" name="value" placeholder="Значение" required />
<!--- placeholder — подсказка в пустом поле --->
<input type="text" name="value" placeholder="Значение" required />
<button type="submit">Добавить</button>
</form>
</cfif>
</body>
</html>
</html>