402 lines
20 KiB
Plaintext
402 lines
20 KiB
Plaintext
<!---
|
||
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>
|