We have a RESTful API which performs some rather sophisticated encryption functions. Our customer is using Teradata and they want to implement this API natively in their database.
The goal is to not store unencrypted values in the database but to make the unencrypted values available when queried.
There are more controls which will be wrapped around this process but in its simplest form, one way to use it is to create Teradata UDFs and use them in:
An After Insert trigger which takes the unencrypted value from the new row, passes it to the API, then takes the result and replaces the unencrypted value with the encrypted value returned by the API.
A view which takes the encrypted value, passes it to our API, and returns the decrypted value in the view result set.
Both the API payload and the returned values are formatted Json and the SQL/UDF will need to get an authentication token to access the API.
We are able to use the API in any number of applications outside of Teradata, including SQL Server, but cannot find documentation to show us how to do this in Teradata. Does anyone have any leads or examples of calling RESTful APIs from within a Teradata UDF?
ALTER FUNCTION [dbo].[GetProfile]
(
#maskProfile tinyint ,
#PolyId nvarchar(50)
)
RETURNS
#profileTable TABLE
(
SourceSystemKey nvarchar(100),
PolyId nvarchar(50),
FirstName nvarchar(100),
LastName nvarchar(100),
DateOfBirth datetime,
Gender nvarchar(10),
EmailAddress nvarchar(100),
PhoneNumber nvarchar(50),
KeyType nvarchar(100),
KeyValue nvarchar(100),
StreetAddress1 nvarchar(100),
StreetAddress2 nvarchar(100),
StreetAddress3 nvarchar(100),
StreetAddress4 nvarchar(100),
City nvarchar(100),
StateCode nvarchar(10),
PostalCode nvarchar(10),
County nvarchar(100),
Country nvarchar(100)
)
AS
BEGIN
DECLARE #method varchar(5)
DECLARE #url varchar(1000)
DECLARE #token nvarchar(2000)
DECLARE #params nvarchar(2000)
DECLARE #tokenResponse nvarchar(max)
DECLARE #profileResponse nvarchar(max)
DECLARE #profileJson nvarchar(max)
DECLARE #vaultUri nvarchar(100) = 'http://xxxxx'
SET #method='POST';
SET #url = #vaultUri + '/passthrough/api/auth/login';
SET #token=''
SET #params = '{"AccountId": "<account id>",' +
' "ApiKey": "<api key>"}';
select #tokenResponse = dbo.GetPostAPI(#method,#url,#token,#params)
set #token =
(SELECT * FROM OPENJSON(#tokenResponse, N'$.Data')
WITH (
[token] nvarchar(max) N'$.Token'
))
set #method = 'PUT';
set #url = #vaultUri + '/piivault/api/profiles/GetProfile';
set #params = '{
"Seed": 0,
"Ids": [
{ "Index": 0,
"PolyId": "' + #PolyId + '" } ]
}';
set #profileResponse = dbo.GetPostAPI(#method,#url,#token,#params) ;
select #profileJson = JSON_QUERY (#profileResponse, '$.Data.Profiles')
select #profileJson = SUBSTRING(#profileJson, 2, len(#profileJson) - 2)
insert into #profileTable
select top 1 *
from GetProfileAttributes(#profileJson)
RETURN
END
GO
```
```
ALTER function [dbo].[GetPostAPI]
(
#method varchar(5),
#url varchar(8000),
#token NVARCHAR(2000),
#params NVARCHAR(2000)
)
returns varchar(8000)
as
BEGIN
DECLARE #authHeader NVARCHAR(2000);
DECLARE #contentType NVARCHAR(64);
DECLARE #postData NVARCHAR(2000);
DECLARE #responseText NVARCHAR(2000);
DECLARE #responseXML NVARCHAR(2000);
DECLARE #ret INT;
DECLARE #status NVARCHAR(32);
DECLARE #statusText NVARCHAR(32);
DECLARE #internalToken INT;
SET #authHeader= 'Bearer ' + #token;
SET #contentType = 'application/json';
EXEC #ret = sp_OACreate 'WinHttp.WinHttpRequest.5.1', #internalToken OUT;
-- Send the request.
EXEC #ret = sp_OAMethod #internalToken, 'open', NULL, #method, #url, 'false';
EXEC #ret = sp_OAMethod #internalToken, 'setRequestHeader', NULL, 'Authorization', #authHeader;
if #ret <> 0
begin
set #ret = 'sp_OAMethod setRequestHeader failed'
end
EXEC #ret = sp_OAMethod #internalToken, 'setRequestHeader', NULL, 'Content-type', #contentType;
EXEC #ret = sp_OAMethod #internalToken, 'send', NULL, #params;
-- Handle the response.
EXEC #ret = sp_OAGetProperty #internalToken, 'status', #status OUT;
EXEC #ret = sp_OAGetProperty #internalToken, 'statusText', #statusText OUT;
EXEC #ret = sp_OAGetProperty #internalToken, 'responseText', #responseText OUT;
-- Close the connection.
EXEC #ret = sp_OADestroy #internalToken;
return #responseText
END
GO
```
```
ALTER view [dbo].[v_xxx]
as
select ff.*, profile.FirstName, profile.LastName, profile.EmailAddress
from ff_flights_Anonymized ff
cross apply GetProfile(0, ff.mileageplus_number_PolyId) profile
GO
`
```
Related
I'm working with SQL Server 2019 and pulling data from MercadoLibre API as JSON. Here's an example of a simple detail product:
https://api.mercadolibre.com/items?ids=MLA1133834371&attributes=title,price,original_price,id,tltle,last_updated,attributes.id,attributes.value_name
This outputs a Json that looks like this:
{"code":200,"body":{"price":835,"original_price":null,"id":"MLA1133834371","last_updated":"2023-01-20T00:46:01.000Z","attributes":[{"id":"BRAND","value_name":"Energizer"},{"id":"CELL_BATTERY_CAPACITY","value_name":null},{"id":"CELL_BATTERY_COMPOSITION","value_name":"Alcalino"},{"id":"CELL_BATTERY_SHAPE","value_name":"Rectangular"},{"id":"CELL_BATTERY_SIZE","value_name":"9V"},{"id":"COLOR","value_name":"Plateado"},{"value_name":null,"id":"ENVIRONMENTAL_MANAGEMENT_PLAN_AUTHORIZATION"},{"id":"GTIN","value_name":"039800013613"},{"value_name":"Sí","id":"IS_FLAMMABLE"},{"id":"IS_RECHARGEABLE","value_name":"No"},{"value_name":"Nuevo","id":"ITEM_CONDITION"},{"value_name":"MAX","id":"LINE"},{"id":"MAX_LOAD_HOLDING_TIME","value_name":"5 años"},{"id":"MODEL","value_name":"522"},{"id":"NOMINAL_VOLTAGE","value_name":"9V"},{"value_name":"3.4 cm","id":"PACKAGE_HEIGHT"},{"id":"PACKAGE_LENGTH","value_name":"6.6 cm"},{"id":"PACKAGE_WEIGHT","value_name":"140 g"},{"id":"PACKAGE_WIDTH","value_name":"6.6 cm"},{"id":"PRODUCT_FEATURES","value_name":"Con vencimiento"},{"id":"RECOMMENDED_USES","value_name":"Control remoto,Juguetes,Linterna,Reloj"},{"id":"SALE_FORMAT","value_name":"Unidad"},{"id":"SELLER_SKU","value_name":"ENE1XALC9V"},{"value_name":"1","id":"UNITS_PER_PACK"},{"id":"WITH_MERCURY","value_name":"No"}],"title":"Pila 9v Energizer Max 522 Rectangular - 1 Unidad"}}
Problem is that the "attributes" fields comes in different order for every item you query, so far I've tried:
DECLARE #url VARCHAR(max) = 'https://api.mercadolibre.com/items?ids=MLA1133834371&attributes=title,price,original_price,id,tltle,last_updated,attributes.id,attributes.value_name'
DECLARE #token as int;
DECLARE #respuesta as VARCHAR(8000);
DECLARE #ret INT;
EXEC sp_OACreate 'MSXML2.XMLHTTP', #token OUT;
IF #ret <> 0 RAISERROR('Unable to open HTTP connection.', 10, 1);
EXEC #ret = sp_OAMethod #token, 'open', NULL, 'GET', #url, 'false';
EXEC #ret = sp_OAMethod #token, 'send'
EXEC #ret = sp_OAMethod #token, 'responseText', #respuesta OUTPUT
SET #respuesta = TRIM('[]' FROM #respuesta)
SELECT JSON_VALUE(#respuesta,'$.body.id'),
JSON_VALUE(#respuesta,'$.body.title'),
JSON_VALUE(#respuesta,'$.body.price'),
JSON_VALUE(#respuesta,'$.body.last_updated'),
JSON_VALUE(#respuesta,'$.body.attributes[7].id'),
JSON_VALUE(#respuesta,'$.body.attributes[7].value_name')
EXEC sp_OADestroy #token
How can i find this GTIN field without knowing it's position in the array?
Thanks
You can use OPENJSON
SELECT value_name
FROM OPENJSON(#respuesta, '$.body.attributes')
WITH (id VARCHAR(100), value_name VARCHAR(100))
WHERE id = 'GTIN'
You might also be better off using that for the other values too. If you are interested in additional attributes you can consider PIVOT (dbfiddle)
SELECT title, price, last_updated, attrib.*
FROM OPENJSON(#respuesta, '$.body')
WITH (title VARCHAR(100),
price INT,
last_updated DATETIME2(0),
attributes NVARCHAR(MAX) AS JSON)
OUTER APPLY
(
SELECT *
FROM OPENJSON(attributes)
WITH (id VARCHAR(100), value_name VARCHAR(100))
PIVOT (MAX(value_name) FOR id IN (GTIN, SELLER_SKU)) P
) attrib
I am exporting blobs with this code:
sp_configure 'Ole Automation Procedures', 1;
GO
RECONFIGURE;
GO
DECLARE #outout_path varchar(50) = 'D:\blob',
#i bigint,
#init int,
#data varbinary(max),
#file_path varchar(max),
#folder_path varchar(max)
DECLARE #Doctable TABLE (id int identity(1,1) , [FileName] varchar(100), file_data varBinary(max) )
INSERT INTO #Doctable([FileName],file_data)
Select top 10 thefilename, file_data FROM schm.table_with_blobs
SELECT #i = COUNT(1) FROM #Doctable
WHILE #i >= 1
BEGIN
SELECT #data = [file_data],
#file_path = #outout_path + '\'+ cast(id as varchar) + '\' + [FileName],
#folder_path = #outout_path + '\'+ cast(id as varchar)
FROM #Doctable
WHERE id = #i
EXEC [dbo].[CreateFolder] #folder_path
EXEC sp_OACreate 'ADODB.Stream', #init OUTPUT;
EXEC sp_OASetProperty #init, 'Type', 1;
EXEC sp_OAMethod #init, 'Open';
EXEC sp_OAMethod #init, 'Write', NULL, #data;
EXEC sp_OAMethod #init, 'SaveToFile', NULL, #file_path, 2;
EXEC sp_OAMethod #init, 'Close';
EXEC sp_OADestroy #init;
print 'Document Generated at - '+ #file_path
SELECT #data = NULL,
#init = NULL,
#file_path = NULL,
#folder_path = NULL;
SET #i -= 1;
END
All the files are exported as expected with the proper file format. However, all of the files are corrupted and cannot be opened, regardless of file format. What can I tweak to avoid this? Do I need to more explicitly call out the file type (available in thefilename field)?
I had a similar problem but was exporting with BCP rather than using ADODB.
I had to do two things :
convert the varbinary to varchar or nvarchar
make BCP export as
RAW
This is the BCP solution I came up with:
Declare #patient int, #mincount int, #maxcount int,#filename varchar(200), #script varchar(2000),#fileid varchar(10)
Set #patient=2;
set #mincount=1;
Declare #mydocs as table
(filename varchar(255), filebody image, drank int, file_id int)
insert into #mydocs
Select taf.Attachment_File_Name, taf.Attachment_File_Body, DENSE_RANK() OVER (ORDER BY taf.attachment_file_id) as drank, taf.Attachment_File_ID FROM [CNGSTT].[dbo].[tblAttachment] ta
left join [CNGSTT].[dbo].[tblAttachmentFile] taf
on taf.Attachment_ID=ta.Attachment_ID where ta.Patient_ID=#patient
Set #maxcount=(select MAX(drank) from #mydocs)
WHILE #mincount<=#maxcount
BEGIN
Set #filename = (select fle.FileName from #mydocs fle where drank=#mincount)
Set #fileid =(select fle.File_id from #mydocs fle where drank=#mincount)
set #script = 'bcp "SELECT cast(cast(Attachment_File_Body as varbinary(max)) as varchar(max)) from [tblattachmentfile] fle where Attachment_File_ID='+#fileid+'" queryout D:\temp\' + #filename + ' -T -c -C RAW'
exec master..xp_cmdshell #script
Set #mincount=#mincount+1
END
My client wants an insert trigger on his Order table (from Sage) to create a Freshdesk ticket using the API.
As part of my development, I built a stored procedure that does the job fine when provided with an order number. However, transplanting the same code into a trigger returns without error, but nothing appears in the Freshdesk system, when the same code in a stored procedure works.
I expect comments about why an API call in a trigger might be a bad idea, but the Freshdesk call is very quick (<1 second from the stored procedure).
What I'd like to know is -- Is this is architecturally forbidden by SQL Server for some reason? If it's allowed, where might I look for the error that's being thrown.
Edit2: OK, here's the whole trigger .. pervious version just had OA calls.
ALTER TRIGGER [dbo].[CreateFreshdeskTicketFromOrder]
ON [dbo].[OEORDH]
AFTER INSERT
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Get the original order number, and use that in the main lookup query
DECLARE #ORDNUM VARCHAR(22)
SELECT #ORDNUM = ORDNUMBER FROM inserted
-- Variables for fields going to the API
DECLARE #EMAIL VARCHAR(60), #SHPCONTACT VARCHAR(60), #ORDNUMBER VARCHAR(22)
DECLARE #LOCATION VARCHAR(6), #EXPDATE INT
DECLARE #SHPPHONEC VARCHAR(30), #SHPNAME VARCHAR(60), #DESCR VARCHAR(60)
DECLARE #CODEEMPL VARCHAR(15)
-- Collect field values that were just inserted
SELECT
#EMAIL = rtrim(OEORDH1.SHPEMAILC), #SHPCONTACT = rtrim(SHPCONTACT),
#ORDNUMBER = rtrim(ORDNUMBER), #LOCATION = LOCATION, #EXPDATE = EXPDATE,
#SHPPHONEC = rtrim(OEORDH1.SHPPHONEC), #SHPNAME = SHPNAME,
#DESCR = rtrim([DESC]), #CODEEMPL = rtrim(ARSAP.CODEEMPL)
-- FROM inserted
FROM dbo.OEORDH
JOIN dbo.OEORDH1 on dbo.OEORDH.ORDUNIQ = dbo.OEORDH1.ORDUNIQ
JOIN dbo.ARSAP on dbo.OEORDH.SALESPER1 = dbo.ARSAP.CODESLSP
WHERE ORDNUMBER = #ORDNUM
-- Variables from database to the API call
DECLARE #EXPDATE_OUT VARCHAR(10)
SET #EXPDATE_OUT =
substring ( cast ( #EXPDATE as varchar(8) ), 1, 4 ) + '-' +
substring ( cast ( #EXPDATE as varchar(8) ), 5, 2 ) + '-' +
substring ( cast ( #EXPDATE as varchar(8) ), 7, 2 );
DECLARE #STATUS_OUT VARCHAR(2)
IF #LOCATION = '1A'
SET #STATUS_OUT = '23';
ELSE
IF #LOCATION = '1'
SET #STATUS_OUT = '40';
ELSE
SET #STATUS_OUT = '2';
-- Variables for building the API call
DECLARE #Object INT
DECLARE #Url VARCHAR(80)
DECLARE #Body1 VARCHAR(1000) =
'{ ' +
'"email": "'+ #EMAIL +'", ' +
'"custom_fields": { "order_number": "'+ #ORDNUMBER +'", "scheduled_date": "'+ #EXPDATE_OUT + '", ' +
'"delivered_to": "'+ #SHPCONTACT + '", ' + '"consignee_phone_number": "'+ #SHPPHONEC +'" }, ' +
'"status": '+ #STATUS_OUT + ', ' +
'"priority": 1, "subject": "'+ rtrim(#ORDNUMBER) + ' - ' + rtrim(#SHPNAME) + ' (' + #DESCR + ')", ' +
'"responder_id": ' + #CODEEMPL +
' }'
DECLARE #ResponseText VARCHAR(1000), #return_status INT
SET #Url = 'https://client.freshdesk.com/api/v2/tickets';
-- Do REST call to API / All return statuses commented out except for last
Exec #return_status = sp_OACreate 'MSXML2.ServerXMLHTTP', #Object OUT;
-- Select 'Create return', #return_status
Exec #return_status = sp_OAMethod #Object, 'Open', NULL, 'POST', #Url, false
-- Select 'Open return', #return_status
Exec #return_status = sp_OAMethod #Object, 'setRequestHeader', NULL,
'Content-Type', 'application/json'
-- Select 'Set Request Header1 return', #return_status
Exec #return_status = sp_OAMethod #Object, 'setRequestHeader', NULL,
'Authorization', 'Basic ABC123=='
-- Select 'Set Request Header2 return', #return_status
Exec #return_status = sp_OAMethod #Object, 'Send', NULL, #Body1
-- Select 'Send1 return', #return_status
Exec sp_OAMethod #Object, 'ResponseText', #ResponseText OUT
-- Select 'Response', #ResponseText
Exec sp_OADestroy #Object
-- Add the conversation to the TriggerLog
IF #ResponseText IS NULL
SET #ResponseText = '(Null)';
INSERT INTO dbo.TriggerLog (tl_source, tl_input, tl_output) VALUES
( 'FreshdeskInsertTrigger', #Body1, #ResponseText )
END
That's the trigger code.
A stored procedure that has the same code (but takes an order number as a parameter) works correctly and does the API call and does the logging. Commenting out the logging at the end of the trigger made the error from Sage go away, but the API call still didn't arrive.
What happens if you simply call your working stored procedure from the trigger (via EXEC) instead of including the procedure's code in the trigger?
One thing to look closely at is this place in the code:
-- Collect field values that were just inserted
SELECT
#EMAIL = rtrim(OEORDH1.SHPEMAILC), #SHPCONTACT = rtrim(SHPCONTACT),
#ORDNUMBER = rtrim(ORDNUMBER), #LOCATION = LOCATION, #EXPDATE = EXPDATE,
#SHPPHONEC = rtrim(OEORDH1.SHPPHONEC), #SHPNAME = SHPNAME,
#DESCR = rtrim([DESC]), #CODEEMPL = rtrim(ARSAP.CODEEMPL)
-- FROM inserted
FROM dbo.OEORDH
JOIN dbo.OEORDH1 on dbo.OEORDH.ORDUNIQ = dbo.OEORDH1.ORDUNIQ
JOIN dbo.ARSAP on dbo.OEORDH.SALESPER1 = dbo.ARSAP.CODESLSP
WHERE ORDNUMBER = #ORDNUM
You commented out FROM inserted and try to read values from the table directly.
When the trigger code runs the transaction is not committed yet, so most likely you should read the values from the inserted table. It is likely that this SELECT doesn't find a row with the given #ORDNUM and variables remain NULL.
Side note. Triggers in SQL Server are fired once per the statement, not once per row. Your trigger should work correctly even if inserted table has several rows. Right now your trigger would pick only one ORDNUMBER even if several rows were inserted into the table. Most likely this is not what you want.
How to debug the trigger?
One simple and very straight-forward way is to create a table for logging and add plenty of INSERT statements that would log values of all variables into that table. Then you can read through the logs and see what was going on.
I have a stored procedure that outputs a select statement as you see below:
select case when count(*) > 10 then ''A1'' else ''B1'' end as id, name, address from database.dbo.student
Now I want to write a stored procedure that takes such a select statement as string input, and return all the above outputs. I'm not sure where to assign the output variables.
ALTER PROCEDURE dbo.selectAttributes
#select_statement Nvarchar(MAX),
#id Nvarchar(255) OUT,
#name nvarchar(255) OUT,
#address nvarchar(255) OUT
AS
BEGIN
DECLARE #query nvarchar(MAX)
SET #query = #select_statement
EXEC sp_executesql #query,
N'#select_statement Nvarchar(MAX),
#idOUT Nvarchar(255) OUTPUT,
#nameOUT nvarchar(255) OUTPUT,
#addressOUT nvarchar(255) OUTPUT',
#select_statement, #id OUTPUT, #name OUTPUT, #address OUTPUT
END
I think you are close:
ALTER PROCEDURE dbo.selectAttributes (
#select_statement Nvarchar(MAX),
#id Nvarchar(255) OUTPUT,
#name nvarchar(255) OUTPUT,
#address nvarchar(255) OUTPUT
) AS
BEGIN
EXEC sp_executesql #select_statement,
N'#id Nvarchar(255) OUTPUT, #name nvarchar(255) OUTPUT, #address nvarchar(255) OUTPUT',
#id = #id OUTPUT,
#name = #name OUTPUT,
#address = #address OUTPUT
END;
In other words:
You have to put the OUTPUT keyword everywhere.
The query string does not go in as a parameter.
I don't rename the parameters, but if you do, they have to be declared somewhere.
Can't seem to find much information about how to attach a file that's been stored as a BLOB as part of an email.
I know you can attach files from the file system (C:\temp...) to email that are being setup in DBMAIL or custom stored procedures. However, I haven't seen any references to attaching something such as a PDF that's been stored as a binary object in a table.
I see where you can attach a query as a file, but I don't think that's what I'm looking for.
We would do this programatically via the application, but we need to be able to kick this SP via triggers or the application's server side code.
Is this possible or should i be asking other questions, since usually a binary object also needs to have a content type associated with it for browsers or mail objects to know how to handle?
The solution can not attach binary object stored in db field, you may change your schema a little bit store the path to the binary file. if you could enable .net clr in your database server, you will have more options.
use master
go
if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[usp_send_cdosysmail]')
and objectproperty(id, N'isprocedure') = 1)
drop procedure [dbo].[usp_send_cdosysmail]
go
create procedure usp_send_cdosysmail
#from varchar(500) ,
#to varchar(500) ,
#subject varchar(500),
#body nvarchar(max) ,
#smtpserver varchar(25),
#bodytype varchar(10) ,
#attachment varchar(100)= ' '
as
declare #imsg int
declare #hr int
declare #source varchar(255)
declare #description varchar(500)
declare #output varchar(1000)
exec #hr = sp_oacreate 'cdo.message', #imsg out
exec #hr = sp_oasetproperty #imsg,
'configuration.fields("http://schemas.microsoft.com/cdo/configuration/sendusing").value','2'
exec #hr = sp_oasetproperty #imsg, 'configuration.fields("http://schemas.microsoft.com/cdo/configuration/smtpserver").value', #smtpserver
exec #hr = sp_oamethod #imsg, 'configuration.fields.update', null
exec #hr = sp_oasetproperty #imsg, 'to', #to
exec #hr = sp_oasetproperty #imsg, 'from', #from
exec #hr = sp_oasetproperty #imsg, 'subject', #subject
-- if you are using html e-mail, use 'htmlbody' instead of 'textbody'.
exec #hr = sp_oasetproperty #imsg, #bodytype, #body
-- Attachments...
IF #attachment IS NOT NULL AND LEN(#attachment) > 0 BEGIN
Declare #files table(fileid int identity(1,1),[file] varchar(255))
Declare #file varchar(255)
Declare #filecount int ; set #filecount=0
Declare #counter int ; set #counter = 1
DECLARE #outVar INT
SET #outVar = NULL
INSERT #files SELECT cValue FROM master..fn_split(#attachment,',')
SELECT #filecount=##ROWCOUNT
WHILE #counter<(#filecount+1)
BEGIN
SELECT #file = [file]
FROM #files
WHERE fileid=#counter
EXEC #hr = sp_OAMethod #imsg, 'AddAttachment',#outVar OUT, #file
SET #counter=#counter+1
END
END
exec #hr = sp_oamethod #imsg, 'send', null
-- sample error handling.
if #hr <>0
select #hr
begin
exec #hr = sp_oageterrorinfo null, #source out, #description out
if #hr = 0
begin
select #output = ' source: ' + #source
print #output
select #output = ' description: ' + #description
print #output
end
else
begin
print ' sp_oageterrorinfo failed.'
return
end
end
exec #hr = sp_oadestroy #imsg
go
set quoted_identifier off
go
set ansi_nulls on
go
sp_configure 'Ole Automation Procedures', 1
RECONFIGURE
GO