Files
svc-api-x/v1/resources/instance_ls.cfc
T
2024-12-23 12:28:39 +03:00

333 lines
21 KiB
Plaintext

<cfcomponent extends="taffy.core.resource" taffy:uri="/instances">
<cfsilent>
<cfimport prefix="m" taglib="../lib"/>
<cfset this.helper=CreateObject("component","lib.rest_api_helper")/><!---*** странно, почему мы его видим?---><!---вынести в апп?--->
</cfsilent>
<!---спецификация полей, пригодных для фильтрации (?и сортировки)--->
<cfset this.fieldsSpec={
"instance_uid"={prefix="inr", type="guid"},
"dt_created"={prefix="inr", type="date"},
"dt_updated"={prefix="inr", type="date"},
"instance_config_dt_created"={prefix="inr", type="date"},
"instance_config_dt_updated"={prefix="inr", type="date"},
"display_name"={prefix="inr", type="string"},
"service_id"={prefix="inr", type="integer"},
"specification_item_id"={prefix="inr", type="integer"},
"contract_id"={prefix="inr", type="integer"},
"specification_id"={prefix="inr", type="integer"},
"svc"={prefix="inr", type="string"},
"code"={prefix="inr", type="string"},
"last_operation"={prefix="inr", type="string"},
"last_operation_uid"={prefix="inr", type="guid"},
"submit_result"={prefix="inr", type="string"},
"operation_dt_start"={prefix="inr", type="date"},
"operation_dt_finish"={prefix="inr", type="date"},
"duration"={prefix="inr", type="numeric"},
"uptime"={prefix="inr", type="numeric"},
"dt_deploy"={prefix="inr", type="date"},
"instance_data"={prefix="inr", type="string"},
"instance_state_uid"={prefix="inr", type="guid"},
"is_deleted"={prefix="inr", type="string"},<!--- not bool --->
"suspended"={prefix="inr", type="string"},<!--- not bool --->
"is_test"={prefix="inr", type="boolean"},
"explained_status"={prefix="inr", type="string"},
"version"={prefix="inr", type="integer"}
}
/>
<cffunction name="get" hint="Список экземпляров сервисов, принадлежащих данному клиенту">
<cfargument name="pageSize" type="string" hint="type:integer" default="100"/>
<cfargument name="page" type="string" hint="type:integer" default="1"/>
<cfargument name="orderBy" type="string" hint="type:string, description:comma-separated list of fields to sort by, example:fld1.ASC,fld2.DESC,fld3.ASC (performance not assured on arbitrary sorting)" default=""/>
<cfargument name="fields" type="string" hint="type:string, description:comma-separated list of fields to limit output to, empty string for default field set" default=""/>
<!---сюда можно воткнуть спецификацию полей для сортировки и фильтрации, но только ручками, идея компактного кода еще более отдаляется--->
<!---parse and validate request parameters--->
<cfset local={}/>
<cftry>
<cfset this.helper.validateField(arguments, "pageSize", "integer")/>
<cfset this.helper.validateField(arguments, "page", "integer")/>
<!---мы мирно игнорируем поля, отсутствующие в спецификации, что позволяет не делать исключения для orderBy и т.п.--->
<cfset var filter=this.helper.parseFilterParams(this.fieldsSpec)/>
<cfset var order=this.helper.parseOrderBy(this.fieldsSpec, arguments.orderBy)/>
<cfset local.fields=""/>
<cfloop collection=#this.fieldsSpec# item="fld"><cfoutput>#this.helper.snake2camel(trim(fld))#</cfoutput>
<cfif listFind(arguments.fields, this.helper.snake2camel(trim(fld)))
OR structKeyExists(order,this.helper.snake2camel(trim(fld)))>
<cfset local.fields=listAppend(local.fields,trim(fld))/>
</cfif>
</cfloop>
<cfcatch type="invalidParamValue">
<cfreturn representationOf(this.helper.formatBadRequestError(cfcatch)).withStatus(400)/>
</cfcatch>
</cftry>
<cftry><!--- *** времянка, пока глотается ошибка 500 --->
<cfset var maxrows=arguments.pageSize*arguments.page/>
<cfset var startrow=arguments.pageSize*(arguments.page-1)+1/>
<!--- Такая бодяга. Кастомные сериализаторы не умеют вложенных структур (поразительно, или я не нашел как). Стандартный сериализатор не умеет выводить дату в ISO 8601. Приходится колхозить и форматировать на стороне БД (насилу подобрал формат). Также на стороне БД приходится конвертировать GUID, потому что драйвер pg jdbc его представляет как структуру из 2 чисел.
И то сказать, jsonb как-то странно сериализуется, в 3 поля, вместо одного Value
--->
<cfquery name="local.qRead" result="local.result">
select
<cfif listLen(local.fields)>#local.fields#<cfelse>*</cfif>
from (
select
<m:field_set titleMapOut="local.titleMap" lengthOut="local.fieldCount">
<m:field>to_char(e.dt_created, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZHTZM') as instance_config_dt_created</m:field><!--- to_json(e.dt_created)##>>'{}' --->
<m:field>to_char(e.dt_updated, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZHTZM') as instance_config_dt_updated</m:field>
<m:field>e.instance_uid::text as instance_uid</m:field>
<m:field>e.display_name</m:field>
<m:field>e.service_id</m:field>
<m:field>e.specification_item_id</m:field>
<m:field>d.contract_id</m:field>
<m:field>i.specification_id</m:field>
<m:field>s.specification</m:field>
<m:field>i.quantity</m:field>
<m:field>i.price</m:field>
<m:field>v.svc</m:field>
<m:field>v.code</m:field>
<!--- <m:field>(select operation from instance_state st join instance_operation io on (st.instance_operation_uid=io.instance_operation_uid) where st.instance_uid=e.instance_uid order by version desc limit 1) as operation</m:field>
<m:field>(select instance_operation_uid::text from instance_state st where st.instance_uid=e.instance_uid order by version desc limit 1) as instance_operation_uid</m:field> --->
<m:field>(select io.operation from instance_operation io where io.instance_uid=e.instance_uid order by dt_submit desc limit 1) as last_operation</m:field>
<m:field>(select io.instance_operation_uid::text from instance_operation io where io.instance_uid=e.instance_uid order by dt_submit desc limit 1) as last_operation_uid</m:field>
<m:field>(select io.submit_result from instance_operation io where io.instance_uid=e.instance_uid order by dt_submit desc limit 1) as submit_result</m:field><!--- *** последняя операция может быть неуспешной, и даже не отправленой --->
<m:field>(select to_char(io.dt_start, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZHTZM') from instance_operation io where io.instance_uid=e.instance_uid order by dt_submit desc limit 1) as operation_dt_start</m:field>
<m:field>(select to_char(io.dt_finish, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZHTZM') from instance_operation io where io.instance_uid=e.instance_uid order by dt_submit desc limit 1) as operation_dt_finish</m:field>
<m:field>(select round(extract('epoch' from coalesce(io.dt_finish,CURRENT_TIMESTAMP)- io.dt_start),1) from instance_operation io where io.instance_uid=e.instance_uid order by dt_submit desc limit 1) as duration</m:field>
<m:field>(select case when io.is_successful then extract('epoch' from CURRENT_TIMESTAMP - coalesce(io.dt_finish,CURRENT_TIMESTAMP)) else 0 end from instance_operation io where io.instance_uid=e.instance_uid order by dt_submit desc limit 1) as uptime</m:field>
<m:field>(select to_char(io.dt_finish, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZHTZM') from instance_operation io where io.instance_uid=e.instance_uid AND io.is_successful AND io.operation='create' order by dt_submit desc limit 1) as dt_deploy</m:field>
<!--- *** не то же самое, что в DETAIL view! --->
<m:field formatter=#function(x){return (deserializeJson(x))}#>(select instance_data::text from instance_state st where st.instance_uid=e.instance_uid order by version desc limit 1) as instance_data</m:field><!--- *** операция выглядит очень накладно, но можно это поле в дельнейшем не выводить --->
<m:field>(select st.instance_state_uid::text from instance_state st where st.instance_uid=e.instance_uid order by version desc limit 1) as instance_state_uid</m:field>
<m:field>(select st.is_test from instance_state st where st.instance_uid=e.instance_uid order by version desc limit 1) as is_test</m:field>
<m:field>(select st.instance_data->>'isDeleted' from instance_state st where st.instance_uid=e.instance_uid order by version desc limit 1) as is_deleted</m:field>
<m:field>(select st.instance_data->>'suspended' from instance_state st where st.instance_uid=e.instance_uid order by version desc limit 1) as suspended</m:field>
<m:field>(select to_char(st.dt_state, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZHTZM') from instance_state st where st.instance_uid=e.instance_uid order by version desc limit 1) as dt_state</m:field>
<!--- <m:field>(select st.instance_data->>'grafanaUrl' from instance_state st where st.instance_uid=e.instance_uid order by version desc limit 1) as grafana_url</m:field> --->
<m:field>
(case
when
(select st.instance_data->>'isDeleted' from instance_state st where st.instance_uid=e.instance_uid order by version desc limit 1) = 'true' then 'deleted'
when
(select st.instance_data->>'suspended' from instance_state st where st.instance_uid=e.instance_uid order by version desc limit 1) = 'true' then 'suspended'
when
(select COALESCE(st.instance_data->>'isDeleted','false') from instance_state st where st.instance_uid=e.instance_uid order by version desc limit 1) = 'false' AND (select COALESCE(st.instance_data->>'suspended','false') from instance_state st where st.instance_uid=e.instance_uid order by version desc limit 1) = 'false' then 'running'
when
not exists (select * from instance_operation o where o.instance_uid=e.instance_uid) then 'not configured'
when
not exists (select * from instance_state st where st.instance_uid=e.instance_uid) then 'not created'
else null end
) as explained_status
</m:field><!--- ******** (сделать тест кейсы) еще ситуация, когда не завершена операция create, но есть первый стейт --->
<m:field>(select max(version) from instance_state st where st.instance_uid=e.instance_uid) as version</m:field>
</m:field_set>
from instance e
join specification_item i on (e.specification_item_id=i.specification_item_id)
join specification s on (i.specification_id=s.specification_id)
join contract d on (s.contract_id=d.contract_id)
join contragent k on (d.contragent_id=k.contragent_id)
join svc v on (e.service_id=v.svc_id)
where s.specification_id=<cfqueryparam cfsqltype="cf_sql_integer" value=#arguments.specificationId#/><!--- *** make project entity --->
<!--- u.usr_id=<cfqueryparam cfsqltype="cf_sql_integer" value=#arguments.usrId#/> --->
) inr
where 1=1 <m:filter_build filter=#filter#/>
<!--- order by <m:order_build sortCollection=#this.helper.parseNumericOrder(local.titleMap, arguments.orderBy)# fieldCount=0/> ---><!--- из-за нефиксированного числа колонок труднее становится использовать цифровую нотацию --->
order by <m:order_build sortCollection=#order# fieldCount=0 defaultOrder="1 desc"/><!---no sort length limit--->
limit #maxrows#
</cfquery>
<!---<cfdump var=#local.qRead#/><cfabort/> --->
<cfquery name="local.qTotal">
select count(*) as cnt
from instance e
join specification_item i on (e.specification_item_id=i.specification_item_id)
join specification s on (i.specification_id=s.specification_id)
join contract d on (s.contract_id=d.contract_id)
join contragent k on (d.contragent_id=k.contragent_id)
--join usr u on (k.contragent_id=u.contragent_id)
join svc v on (e.service_id=v.svc_id)
where s.specification_id=<cfqueryparam cfsqltype="cf_sql_integer" value=#arguments.specificationId#/>
<!--- u.usr_id=<cfqueryparam cfsqltype="cf_sql_integer" value=#arguments.usrId#/> --->
</cfquery>
<cfset var out=structNew("linked")/>
<cfset "out.queryDurationMs"=getTickCount() - request.startTickCount/>
<cfset "out.total"=#local.qTotal.cnt#/>
<cfset var resultSet=[]/>
<cfloop query=#local.qRead# startRow=#startrow#>
<cfset var rec=structNew("linked")/>
<cfset this.helper.appendRecord(rec, "", local.titleMap, local.qRead, this.helper.snake2camel)/>
<cfset arrayAppend(resultSet, rec)/>
</cfloop>
<cfset "out.results"=#resultSet#/>
<cfset "out.pageSize"=#arguments.pageSize#/>
<cfset "out.page"=#arguments.page#/>
<cfset "out.orderBy"=#arguments.orderBy#/>
<cfset "out.size"=#arrayLen(resultSet)#/>
<cfset "out.runDurationMs"=getTickCount()-request.startTickCount/>
<!---<cfset "out.sql"=#local.result.sql#/>--->
<cfreturn representationOf(out)/>
<cfcatch type="any">
<cfreturn representationOf(cfcatch)/>
</cfcatch>
</cftry>
</cffunction>
<cffunction name="post" hint="(ТЕСТИРОВАНИЕ) ВНИМАНИЕ! У текущего контрагента должна быть спецификация, в которую будет вписан создаваемый экземпляр. Новая пустая запись экземпляра. Фактическое развертывание выполняется отдельно,асинхронной операцией create сконфигурированного экземпляра. Одновременно мы создаем строку в спецификации по умолчанию и связываем экземпляр с этой строкой. Первичный ключ генерируется. Текущий пользователь, контракт, спецификация получаются из контекста.">
<cfargument name="displayName" type="string" required=true hint="type:string"/>
<cfargument name="serviceId" type="string" required=true hint="type:integer"/>
<cfargument name="descr" type="string" required=false default="" hint="type:varchar (no cleanup here!)"/>
<!--- <cfargument name="instanceUid" type="string" required=false default="" hint="type:guid)"/> --->
<!--- <cfargument name="usrUid" type="string" required=true hint="type:guid"/>
<cfargument name="companyUid" type="string" required=true hint="type:guid"/> --->
<!--- оказывается, их можно и не декларировать --->
<cfset local={}/>
<!--- <cfif structKeyExists(arguments,"instanceUid")>
<cfset local.instanceUid=arguments.instanceUid/>
<cfelse> --->
<cfset local.instanceUid=#createGUID()#/>
<!--- </cfif> --->
<cftry>
<cfset this.helper.keyExistsAndValid(arguments, "serviceId", "integer")/>
<cfset checkDisplayName(arguments.displayName, arguments.usrId, local.instanceUid)/>
<!--- делаем вставку в спецификацию и таблицу экземпляров --->
<cfquery name="local.qSpecItem">
insert into specification_item
(specification_id, specification_item, svc_id, quantity, price, dt_created, creator_id, dt_updated, updater_id)
values
(
<cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.specificationId#" />
,<cfqueryparam cfsqltype="cf_sql_varchar" value=""/>
,<cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.serviceId#" />
,<cfqueryparam cfsqltype="cf_sql_integer" value="1" /> <!--- это типично для всех PAYG --->
,<cfqueryparam cfsqltype="cf_sql_numeric" null=true />
,<cfqueryparam cfsqltype="cf_sql_timestamp" value="#Now()#" />
,<cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.usrId#" />
,<cfqueryparam cfsqltype="cf_sql_timestamp" value="#Now()#" />
,<cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.usrId#" />
) returning specification_item_id;
</cfquery>
<cfquery name="local.qSave">
insert into instance
(instance_uid, display_name, service_id, specification_item_id, descr, dt_created, creator_id, dt_updated, updater_id)
values
(
<cfqueryparam cfsqltype="cf_sql_other" value="#local.instanceUid#" />
,<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.displayName#"/>
,<cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.serviceId#" />
,<cfqueryparam cfsqltype="cf_sql_integer" value="#local.qSpecItem.specification_item_id#" />
,<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.descr#" />
,<cfqueryparam cfsqltype="cf_sql_timestamp" value="#Now()#" />
,<cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.usrId#" />
,<cfqueryparam cfsqltype="cf_sql_timestamp" value="#Now()#" />
,<cfqueryparam cfsqltype="cf_sql_integer" value="#arguments.usrId#" />
);
</cfquery>
<cfcatch type="invalidParamValue">
<cfreturn representationOf(this.helper.formatBadRequestError(cfcatch)).withStatus(400)/>
</cfcatch>
</cftry>
<cfreturn noData()
.withHeaders({location: "./#local.instanceUid#"})
.withStatus(201, "Created")
/>
</cffunction>
<cffunction name="checkDisplayName">
<cfargument name="displayName" required=true/>
<cfargument name="usrId" type="numeric" required=true/>
<cfargument name="instanceUid" type="guid" required=true/>
<cfset var local={}/>
<cfset var display_name=trim(lcase(arguments.displayName))/>
<cfif len(display_name) LT 3>
<cfthrow type="InvalidParamValue" message="Instance Display Name too short" detail="Имя должно быть не короче 3 символов"/>
<cfelseif len(display_name) GT 80>
<cfthrow type="InvalidParamValue" message="Instance Display Name too long" detail="Имя должно быть не длиннее 80 символов"/>
<cfelse>
<cfquery name="local.qUniqueDisplayName">
select count(*) as cnt from instance e
join specification_item i on (e.specification_item_id=i.specification_item_id)
join specification s on (i.specification_id=s.specification_id)
join contract d on (s.contract_id=d.contract_id)
join contragent k on (d.contragent_id=k.contragent_id)
join usr u on (k.contragent_id=u.contragent_id)
where u.usr_id=<cfqueryparam cfsqltype="cf_sql_integer" value=#arguments.usrId#/>
AND lower(e.display_name)=lower(<cfqueryparam cfsqltype="cf_sql_varchar" value=#arguments.displayName#/>)
AND e.instance_uid <> <cfqueryparam cfsqltype="cf_sql_other" value=#arguments.instanceUid#/>
</cfquery>
<cfif local.qUniqueDisplayName.cnt GT 0>
<cfthrow type="InvalidParamValue" message="Instance Display Name not unique" detail="Имя экземпляра должно быть уникальным в рамках договора"/>
</cfif>
</cfif>
</cffunction>
<cffunction name="generateDefaultName" returnType="string">
<!--- *** Это некорректно, потому что имя может содержать пробелы и т.п. --->
<!--- *** Неуклюже и медленно --->
<!--- Эти валидации и генерации занимают удивительно много рабочего времени --->
<cfargument name="service_id" type="numeric"/>
<cfargument name="usr_id" type="numeric"/>
<cfset var local={}/>
<cfquery name="local.qSvc">
select coalesce(svc_short,svc) as svc_short from svc where svc_id=<cfqueryparam cfsqltype="cf_sql_integer" value=#arguments.service_id#/>
</cfquery>
<cfquery name="local.qInst">
select display_name
from instance e
join specification_item i on (e.specification_item_id=i.specification_item_id)
join specification s on (i.specification_id=s.specification_id)
join contract d on (s.contract_id=d.contract_id)
join contragent k on (d.contragent_id=k.contragent_id)
join usr u on (k.contragent_id=u.contragent_id)
where u.usr_id=<cfqueryparam cfsqltype="cf_sql_integer" value=#arguments.usr_id#/>
AND lower(e.display_name) like lower(<cfqueryparam cfsqltype="cf_sql_varchar" value="#local.qSvc.svc_short#%"/>)
AND service_id=<cfqueryparam cfsqltype="cf_sql_integer" value=#arguments.service_id#/>
</cfquery>
<cfset var prefixLen=len(local.qSvc.svc_short)+1/>
<cfset var i=1/>
<cfloop query="local.qInst">
<cfif len(local.qInst.display_name) GT prefixLen>
<cfset var suffix=right(local.qInst.display_name, len(local.qInst.display_name)-prefixLen)/>
<cfif isValid("integer",suffix)>
<cfset i=(i GT int(suffix))?i:int(suffix)/>
</cfif>
</cfif>
</cfloop>
<cfreturn "#local.qSvc.svc_short#-#i+1#"/>
</cffunction>
</cfcomponent>