spec/lib/data/bean.cfm
2025-06-02 16:16:51 +03:00

402 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!---
bean v0.96
msyu@mail.ru
2013-08-29 v0.1
2013-09-04 v0.2 tested with CF8 and Railo
2013-10-14 added meta attribute - can now export each parameter metadata
2014-12-26 POST-GET read-pass-save
2015-01-19 read-pass-save, добавлен formmarker, убрано определение режима по POST-GET
2015-01-19 убран formmarker, упразднен режим init (используется pass)
2015-07-16 добавлен passmarker для перерисовки формы с введенными значениями
2015-10-02 поддержка составных ключей
2015-10-09 v0.9 если задано value параметра, считается, что он hasInput (корректно работает в составе ключа)
2015-11-25 добавлен readmarker, mode (принудительная установка режима)
2015-11-26 добавлен mode=skip
2016-09-01 изменена трактовка processingMode. Теперь понимается как исходная команда, фиксируется до начала действий. Можно использовать для управления
2018-09-16 добавлена ограниченная поддержка нецелых первичных ключей (idstamp)
child tag: param
to save - should send parameter with name from list ATTRIBUTES.savemarker (value doesn't matter) in FORM, e.g. URL.save="trtrtr", while ATTRIBUTES.savemarker="save,saveAndClose"; for multiple forms on a page (and bean tags) different savemarkers should be used
2022-01-06 PostgreSQL (MSSQL incompatibility because of scope_identity)
2024-04-13 PostgreSQL GUID, autoincrement by RETURNING
2024-09-05 added status.recordExists (*** при вставке принудительно устанавливаем в true, веря, что если insert прошел, то запись появилась)
--->
<!---
Автогенерируемое поле GUID можно заполнять несколькими способами
- как default или init bean/param, но это не очень удобно, потому что может потеряться удобный признак новой записи - инвалидный ключ, и сейчас init-default взаимодействуют некорректно, по крайней мере, неочевидно
- генерируемое в БД (например, как default). Для PostgreSQL значение возвращается через Returning, как это делать для других БД, непонятно.
- можно генерировать при сохранении в bean, но для этого нужно расширение синтаксиса, зато не зависит от БД (*** TODO)
--->
<cfif thisTag.ExecutionMode is 'end'>
<cfparam name="ATTRIBUTES.readmarker" type="string" default="read"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда перечитывания. Имеет приоритет перед savemarker, passmarker. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.savemarker" type="string" default="save,saveAndClose"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда сохранения. Обычно это имя кнопки. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.passmarker" type="string" default="pass"/><!--- наличие в FORM или URL поля с таким именем воспринимается как команда заполнить форму отправленными данными без перечитывания. В форме такое поле обычно есть. Команда сохранения имеет приоритет перед пробросом. Можно передавать несколько вариантов списком через запятую. --->
<cfparam name="ATTRIBUTES.datasource" type="string"/>
<cfparam name="ATTRIBUTES.table" type="string"/>
<cfparam name="ATTRIBUTES.output" type="string"/><!--- имя переменной (структуры) области CALLER, в которую возвращаются данные. Обычно "d" --->
<cfparam name="ATTRIBUTES.info" type="string" default=""/>
<cfparam name="ATTRIBUTES.meta" type="string" default=""/>
<cfparam name="ATTRIBUTES.status" type="string"/><!--- имя переменной (структуры) области CALLER, в которую возвращается статуc. Обычно "status" --->
<cfparam name="ATTRIBUTES.readonly" type="boolean" default="No"/>
<cfparam name="ATTRIBUTES.mode" type="string" default=""/> <!--- Принудительный режим read|pass|save|skip. Имеет приоритет перед маркерами --->
<cfif ATTRIBUTES.mode EQ "skip">
<!---<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorState", false) />
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.errorMessage", "") />
<cfset SetVariable("CALLER.#ATTRIBUTES.status#.processingMode", "skip") />--->
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(false,"","skip",false))/><!--- *** можно было не создавать компонент, он все равно не имеет логики, это map из нескольких полей. С другой стороны, хоть поля декларированы. Не очень удобно, что в отдельном файле --->
<cfexit method="exittag"/>
</cfif>
<cfset validationOk="true" />
<cfset errorState="false" />
<cfset errorMessage="" />
<cfset warningState="false" />
<cfset warningMessage="" />
<!---<cfset keyField=""/>
<cfset keyValue=""/>--->
<cfset hasAutoIncrement="false"/>
<cfset autoIncrementKeyIndex=0/><!--- сгенерированное значение присваивается первому полю, отмеченному как autoincrement --->
<cfset recordExists = "false"/>
<cfset keyInvalid = "false"/>
<!--- разбор метаданных - поиск ключа --->
<!--- поддерживаются только простые целочисленные ключи --->
<cfset keys=arrayNew(1)/>
<!--- *** почему где-то копируем поля param в key, а где-то прямо цикл по параметрам --->
<!--- коллекция thistag.params формируется вложенными тегами param --->
<cfloop array=#thistag.params# index="param">
<!---<cfdump var=#param#/>--->
<cfif structFind(param,"key")><!--- not just structKeyExists! Not exisis, OR exists, but FALSE! --->
<cfset key = structNew()/>
<cfset ArrayAppend(keys, key)/>
<cfset key.name = param.param />
<cfset key.field = param.field />
<cfset key.type = param.type />
<cfset key.value = param.value /><!--- должно уже быть присвоено --->
<cfset key.null = param.null />
<cfif param.errorState OR !param.hasInput>
<cfset keyInvalid="true"/>
<cfelse>
<cfset keyInvalid=!checkDataFormat(param.type, param.value)/>
</cfif>
<cfif !hasAutoIncrement><!--- take only first field with "autoincrement" attribute--->
<cfif structFind(param, "autoincrement")>
<cfset hasAutoIncrement = true/>
<cfset autoIncrementKeyIndex = arrayLen(keys)/>
</cfif>
</cfif>
</cfif>
</cfloop>
<!---<cfdump var=#keys#/>--->
<cfif !keyInvalid>
<!--- *** дублируется похожий cfqueryparam, перепутал один раз --->
<cfquery name="qCheckExistence" datasource="#ATTRIBUTES.datasource#">
select count(*) as cnt
from #ATTRIBUTES.table#
where 1=1
<cfloop array=#keys# index="key">
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
</cfloop>
</cfquery>
<cfset recordExists = (qCheckExistence.cnt GT 0)/>
<!--- <cfdump var=#qCheckExistence#/> --->
<cfelse>
<cfset recordExists = "false"/>
</cfif>
<cfif listFindNoCase("read,pass,save", ATTRIBUTES.mode)>
<cfset processingMode=ATTRIBUTES.mode/>
<cfelse>
<!--- ищем команду принудительного перечитывания --->
<cfset doRead = false/>
<cfloop list=#ATTRIBUTES.readmarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doRead = true/>
<cfbreak/>
</cfif>
</cfloop>
<!--- ищем команду сохранения --->
<cfset doSave = false/>
<cfif !doRead AND !ATTRIBUTES.readonly>
<cfloop list=#ATTRIBUTES.savemarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doSave = true/>
<cfbreak/>
</cfif>
</cfloop>
</cfif>
<!--- ищем команду проброса --->
<cfset doPass = false/>
<cfif !doRead AND !doSave>
<cfloop list=#ATTRIBUTES.passmarker# index="marker">
<cfif structKeyExists(FORM,"#marker#") OR structKeyExists(URL,"#marker#")>
<cfset doPass = true/>
<cfbreak/>
</cfif>
</cfloop>
</cfif>
<!--- определяем режим --->
<cfif doRead>
<cfset processingMode="read"/>
<cfelseif doSave>
<cfset processingMode="save"/>
<cfelse>
<cfif doPass OR !recordExists>
<cfset processingMode="pass"/>
<cfelse>
<cfset processingMode="read"/>
</cfif>
</cfif>
</cfif>
<cfset initialProcessingMode = processingMode/>
<!---debug <cfoutput>processingMode:#processingMode#</cfoutput>--->
<!--- ---------------------------------------------------------- --->
<!---*** Необходимость вставки (против апдейта) определяется по существованию записи. Это небезопасно, поскольку наличие записи с ключом -1 может прекратить создание новых записей. Конечно, можно добавить констрейнт на ключевое поле, но это лишняя зависимость.
Можно явно передавать команду вставки, а не заморачиваться с анализом ключа. Просто традиционно мы сигнализировали необходимость вставки, передавая невалидное значение ключа, обычно -1. Для целочисленных ключей можно было бы просто проверять положительность (что всю жизнь отлично работало), но хочется более общего.
--->
<cfif processingMode IS "save"> <!--- validation lookup --->
<cfloop array=#thistag.params# index="param">
<cfif !param.key>
<cfif param.warningState>
<cfif NOT warningState><!--- catch the first fault --->
<cfset warningMessage=param.warningMessage />
</cfif>
<cfset warningState="true" />
</cfif>
<cfif param.errorState>
<cfif NOT errorState><!--- catch the first fault --->
<cfset errorMessage=param.errorMessage />
</cfif>
<cfset errorState="true" />
<cfset validationOk="false"/>
</cfif>
</cfif>
<cfif errorState>
<!--- clear warning state --->
<cfset warningMessage="" />
<cfset warningState=false />
<!--- finish lookup --->
<cfbreak/>
</cfif>
</cfloop>
<cfif validationOk>
<!---<cfdump var=#param#/> --->
<cftry>
<cfif recordExists><!--- update. Наличие keyValue гарантировано (уверен) --->
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#" result="qSaveResult">
update #ATTRIBUTES.table# set
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<!--- не обновляем ключевые поля --->
<cfif NOT (StructFind(param, "key")
OR StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipUpdate")
)><cfif i++ GT 0>,</cfif>
#param.field#=<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
</cfif>
</cfloop>
where 1=1
<cfloop array=#keys# index="key">
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#"/>
</cfloop>
</cfquery>
<!---<cfdump var=#qSaveResult#/>--->
<cfelse><!--- insert --->
<cfquery name="qSave" datasource="#ATTRIBUTES.datasource#">
insert into #ATTRIBUTES.table#
(
<cfset i=0 />
<!--- ключевые поля без автоинкремента попадают в insert! --->
<cfloop array=#thistag.params# index="param">
<cfif NOT (StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipInsert")
)><cfif i++ GT 0>,</cfif>
#param.field#
</cfif>
</cfloop>
)
values
(
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<cfif NOT (StructFind(param, "autoincrement")
OR StructFind(param, "readonly")
OR StructFind(param, "skipInsert")
)><cfif i++ GT 0>,</cfif>
<cfqueryparam cfsqltype="#sqlType(param.type)#" value="#param.value#" null="#param.null#"/>
</cfif>
</cfloop>
)
<!--- /*MSSQL syntax*/
<cfif hasAutoIncrement>
select scope_identity() as id;
</cfif>
--->
<!---*** PostgreSQL incompatible hack follows--->
<cfif hasAutoIncrement>
returning #thistag.params[autoIncrementKeyIndex].field# as id;
</cfif>;
</cfquery>
<!--- <cfdump var=#qSave#/> --->
<cfif hasAutoIncrement>
<cfset keys[autoIncrementKeyIndex].value=qSave.id/>
</cfif>
<!--- здесь можно установить recordExists=true, если твердо верить, что вставка прошла, если нет исключения --->
<cfset recordExists=true/>
</cfif>
<!--- recordExists --->
<cfset processingMode="read"/>
<cfcatch type="database">
<cfset errorState="true" />
<cfset errorMessage="Ошибка при записи в базу данных. #cfcatch.message# #cfcatch.detail#" />
<cfset processingMode="pass" />
</cfcatch>
</cftry>
<cfelse><!--- (not) validationOk --->
<cfset processingMode="pass"/>
</cfif><!--- validationOk --->
</cfif><!--- processingMode EQ "save" --->
<!--- экспортируем данные --->
<cfswitch expression=#processingMode#>
<cfcase value="read">
<cfquery name="qRead" datasource="#ATTRIBUTES.datasource#">
select
<cfset i=0 />
<cfloop array=#thistag.params# index="param">
<cfset i=i+1/>
#param.field#<cfif i LT ArrayLen(thistag.params)>,</cfif>
</cfloop>
from #ATTRIBUTES.table#
where 1=1
<cfloop array=#keys# index="key">
/*#sqlType(key.type)#*/
AND #key.field#=<cfqueryparam cfsqltype="#sqlType(key.type)#" value="#key.value#" null="#key.null#"/>
</cfloop>
</cfquery>
<cfloop array=#thistag.params# index="param"><!--- *** --->
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", structFind(qRead, #param.field#)) />
</cfloop>
</cfcase>
<cfcase value="pass"><!--- заполнить все поля. Если значения переданы в форме или урле, тег параметра обязан их разобрать и подставить, в противном случае подставить инит-значения. Инит-значение по умолчанию пустая строка, дефолт по умолчанию тоже пустая строка --->
<cfloop array=#thistag.params# index="param">
<!--- <cfdump var=#param#/>--->
<cfif param.hasInput>
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.submittedValue) /><!---*** дефект: 03.12.2015, передаваемая строкой, при обработке dateFormat неправильно парсится --->
<cfelse>
<cfset SetVariable("CALLER.#ATTRIBUTES.output#.#param.param#", param.init) />
</cfif>
</cfloop>
</cfcase>
<cfdefaultcase>
</cfdefaultcase>
</cfswitch>
<cfif len(ATTRIBUTES.info)><!--- экспортируем дополнительные сведения--->
<cfloop array=#thistag.params# index="param">
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.submittedValue", #param.submittedValue#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorState", #param.errorState#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.errorMessage", #param.errorMessage#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningState", #param.warningState#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.warningMessage", #param.warningMessage#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.info#.#param.param#.null", #param.null#) />
</cfloop>
</cfif>
<cfif len(ATTRIBUTES.meta)><!--- экспортируем метаданные параметра - для инвариантности вызовов --->
<cfloop array=#thistag.params# index="param">
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.param", #param.param#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.field", #param.field#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.type", #param.type#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.displayName", #param.displayName#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.default", #param.default#) />
<cfset SetVariable("CALLER.#ATTRIBUTES.meta#.#param.param#.size", #param.size#) />
</cfloop>
</cfif>
<cfset SetVariable("CALLER.#ATTRIBUTES.status#", CreateObject("component","status").init(errorState,errorMessage,initialProcessingMode,recordExists))/>
</cfif><!--- thisTag.ExecutionMode is 'end'--->
<!--- quick and dirty --->
<cffunction name="sqlType">
<cfargument name="type"/>
<cfswitch expression=#type#>
<cfcase value="guid,uuid,idstamp"><cfreturn "CF_SQL_OTHER"/></cfcase><!--- hack for PostgreSQL uuid --->
<cfdefaultcase><cfreturn uCase("CF_SQL_#type#")/></cfdefaultcase>
</cfswitch>
</cffunction>
<cffunction name="checkDataFormat">
<cfargument name="type"/>
<cfargument name="value"/>
<cfswitch expression=#type#>
<cfcase value="integer"><cfreturn isValid("integer",value)/></cfcase>
<cfcase value="guid,uuid,idstamp"><cfreturn isValid("guid",value)/></cfcase>
<cfdefaultcase><cfreturn true/></cfdefaultcase>
</cfswitch>
</cffunction>
<!--- //quick and dirty
function sqlType(type) {
switch (type) {
case "guid,idstamp": return "CF_SQL_OTHER"; /*hack for PostgreSQL uuid*/ /*break not needed!*/
default: return uCase("CF_SQL_#type#");
}
}
function checkDataFormat(type,value) {
switch (type) {
case "integer": return isValid("integer",value); /*break not needed!*/
case "guid,uuid,idstamp": return isValid("guid",value); /*break not needed!*/ /*INCORRECT cfscript switch-case does not support lists*/
default: return true;
}
} --->
<cfscript>
//readonly может транслироваться в pass!
function translateMode(readonly) {
if (readonly) {
return "read";
} else {
return "";
}
}
</cfscript>