SQL Endpoints

SQL Endpoints allow you to build custom API endpoints that use SQL to process the request. Usually these would be SELECT type queries so you can return a JSON object of data, but you can also do INSERT and UPDATE statements.


SQL Endpoints are configured in a new Defined Type called SQL API Endpoints.


All endpoints will be accessed by a URL such as http://rock.rocksolidchurchdemo.com/WebHooks/SQLEndpoint.ashx/XYZ, where XYZ is what you define the endpoint URL (Value) to be. Any hostname that resolves to your Rock site can be used. To gain the benefits of tracking the currently logged in person you will want to use the same hostname that your user is logged into. The easiest way to achive this is just make your AJAX url /Webhooks/SQLEndpoint.ashx/XYZ.

So you know how you can setup routes to Rock pages, like /Person/{PersonId} and it will go to that specific person's page? You can do the same with these SQL Endpoints. So if you set the Value of the endpoint to /attendance/{MetricId} then requesting the endpoint at /Webhooks/SQLEndpoint.ashx/attendance/123 will provide a SQL parameter named @MetricId for your SQL statement with the value of 123.


99% of the time you can probably just leave this blank and everything will work the way you expect. But if you want to limit this SQL to only execute for a specific HTTP Verb, you can specify that verb here. Normally, endpoints that are asking for data from the database use GET and calls that are adding data to the database use POST or PUT. The DELETE verb is used, as you can guess, to delete data from the database. But there is nothing preventing you from running a SQL DELETE statement with a GET verb.


This is, of course, where you can define your SQL to be executed. There will always be two parameters defined for you.

  • @PersonId - If the user requesting access to this endpoint is logged into Rock then this will contain the Id of that Person. Otherwise it will contain the value 0.
  • @Body - If you use a POST, PUT or PATCH request then you are probably sending data along with the request. This data will be made available as-is in the form of the @Body parameter. See below for some creative uses of this value.

Security Roles

Security is important. You want to be able to limit access to specific data. Defined Values do not natively support security settings so instead you can select the security roles that you want to limit access to. If you leave this field blank then all users will be able to access it. The user must match only one of the security roles selected to be granted access.

One gotcha is that there is currently no way to specify "any authenticated user". You either have to allow full anonymous access or limit to a security role.

Optional Parameters

This is where things start to get juicy. You'll want to look over some of the examples to see a bigger picture of how to make use of these, but we'll cover some of the technical details right now. Required Parameters are those that you define in the Value as part of the route. Optional Parameters are those you want to allow the request to specify via query string parameters. As such, they are optional but will always be defined for your SQL code. If they are not provided in the URL then the default value you specify will be used instead.

To make life easier for you, we do a little automatic conversion on optional parameters. If the value can be converted to a decimal or integer value then it will be converted before being passed along to your SQL. This means if you pass 123 as a query string parameter, it will be converted to an integer value of 123. If, however, you specify a value of 123g it will be left as a string because it cannot be converted to either an integer or decimal.

One final feature of optional parameters is that they can be used to receive multiple values. For example, say we want to allow the requester to specify which schedule's to limit the attendance data to, but we want them to be able to specify multiple schedules at one time. In SQL syntax this would be WHERE ScheduleId IN (@ScheduleId). If you specify a parameter multiple times in the query string it will be processed as an array of values. So if you sent a query string of ?ScheduleId=4&ScheduleID=7 then you would effectively end up with this SQL: WHERE ScheduleId IN (4,7).

Note: When specifying the names of the optional parameters do not include a leading @ character. That will be added automatically.


Weekend Service Attendance

Okay, lets assume you have a number of metrics for holding Adult Attendance, High School, Middle School, etc. all the way down to Nursery. Let's call these Metric Ids 720, 721, 722, 723 and 724. Each metric has two partitions. One for campus and one for Schedule. We want to create an endpoint that allows us to retrieve attendance data from those metrics.


    [MV].[YValue] AS [Value],
    [MV].[MetricValueDateTime] AS [DateTime],
    [C].[Name] + ' ' + [S].[Name] AS [Name],
    [C].[Name] AS [Campus],
    [S].[Name] AS [Schedule]
    FROM [MetricValue] AS [MV]
    INNER JOIN [Metric] AS [M] ON [M].[Id] = [MV].[MetricId]
    INNER JOIN [MetricPartition] AS [MPCampus] ON [MPCampus].[MetricId] = [M].[Id] AND [MPCampus].[EntityTypeId] = 67
    INNER JOIN [MetricValuePartition] AS [MVCampus] ON [MVCampus].[MetricValueId] = [MV].[Id] AND [MVCampus].[MetricPartitionId] = [MPCampus].[Id]
    INNER JOIN [Campus] AS [C] ON [C].[Id] = [MVCampus].[EntityId]
    INNER JOIN [MetricPartition] AS [MPSchedule] ON [MPSchedule].[MetricId] = [M].[Id] AND [MPSchedule].[EntityTypeId] = 54
    INNER JOIN [MetricValuePartition] AS [MVSchedule] ON [MVSchedule].[MetricValueId] = [MV].[Id] AND [MVSchedule].[MetricPartitionId] = [MPSchedule].[Id]
    INNER JOIN [Schedule] AS [S] ON [S].[Id] = [MVSchedule].[EntityId]
    WHERE [MV].[MetricId] IN (720, 721, 722, 723, 724)
      AND [MV].[MetricId] IN (@Metric)
      AND (@Schedule = '' OR [S].[Id] IN (@Schedule))
      AND (@Campus = '' OR [C].[Name] IN (@Campus))
      AND [MV].[MetricValueDateTime] >= DATEADD(MONTH, -@Months, GETDATE())
    ORDER BY [MV].[MetricValueDateTime]

The query up until the WHERE statement should be pretty straight forward. We are doing various joins so that we can get the campus and schedule partitions associated with the metric values. We take that data and return enough information to be able to work with the data and know what campus, schedule and metric the value came from.

For this example, lets assume the route is /attendance and we have defined the following optional parameters and their default values.

  • Metric = 720
  • Schedule = empty string
  • Campus = empty string
  • Months = 12

Now, the first thing we do in the WHERE clause is limit our results to a pre-defined set of metric Ids. This is to prevent the user from passing in another metric Id that we did not intend them to have access to. Then we filter again based on what the user provided, which if they did not provide a Metric value then the default of 720 is used.

The next to clauses further filter along the Campus and Schedule parameters. If the user did not provide any values for these then a blank string is used which the filter uses to indicate "show all". Finally we further filter by limiting to the past few months, which by default is 12.

So, to make this easier let's provide a few sample query strings and the resulting WHERE clause. I'm going to specify the WHERE clauses with the parameter values resolved, but rest assured they are actually passed as parameters so there is no worry about the user trying to specify a DROP statement hidden in there somewhere.

WHERE [MV].[MetricId] IN (720, 721, 722, 723, 724)
      AND [MV].[MetricId] IN (720,721)
      AND ('' = '' OR [S].[Id] IN (''))
      AND ('Houston' = '' OR [C].[Name] IN ('Houston'))
      AND [MV].[MetricValueDateTime] >= DATEADD(MONTH, -12, GETDATE())

As you can see, we filter down to values from the Metrics 720 and 721, and only from the "Houston" campus.

WHERE [MV].[MetricId] IN (720, 721, 722, 723, 724)
      AND [MV].[MetricId] IN (720)
      AND ('' = '' OR [S].[Id] IN (''))
      AND ('Houston' = '' OR [C].[Name] IN ('Houston', 'Antonio'))
      AND [MV].[MetricValueDateTime] >= DATEADD(MONTH, -24, GETDATE())

In this query, we are using the default metric Id of 720. But we are requesting values for the campuses "Houston" and "Antonio". We also requested 24 months of data instead of the default 12 months. If you will notice, the campus line checks for 'Houston' = '' even though we specified two values. This has to do with the way we provide the values and detect uses of multi-value checks like the IN keyword. If you want to know the specifics of why this happens, you can read the technical reasons in a later section.

WHERE [MV].[MetricId] IN (720, 721, 722, 723, 724)
      AND [MV].[MetricId] IN (720, 721, 722)
      AND (3 = '' OR [S].[Id] IN (3,4))
      AND ('' = '' OR [C].[Name] IN (''))
      AND [MV].[MetricValueDateTime] >= DATEADD(MONTH, -12, GETDATE())

In this final query, we want the values of 3 metrics and 2 schedules. For example, these two schedule Ids might coincide to our Sunday morning services, excluding the Saturday night service.

Since you are not limited to simply retrieving data from the database, here is an example of using an INSERT and UPDATE statement to set the value of a person's attributes.


IF @AttributeId IN (969, 970, 971) AND @PersonId != 0
    IF EXISTS (SELECT * FROM [AttributeValue] WHERE AttributeId = @AttributeId AND EntityId = @PersonId)
        UPDATE [AttributeValue]
            SET [Value] = @Body
            WHERE [AttributeId] = @AttributeId AND [EntityId] = @PersonId
        INSERT INTO [AttributeValue]
            ([IsSystem], [AttributeId], [EntityId], [Value], [Guid])
            (0, @AttributeId, @PersonId, @Body, NEWID())

First off, we check to make sure the specified attribute is an allowed one. On our system, those three attribute Ids are for the Twitter, Facebook and Instagram attributes. We also check to ensure the user is logged in. Next we check if an AttributeValue already exists. If so then we issue an UPDATE statement to update the existing value. If not then we issue an INSERT statement to create a new attribute value.

Now notice the @Body parameter is being used but hasn't been defined. This is defined for every request and contains any data sent along with a POST, PUT or PATCH request. Let's examine a jQuery request to update the Twitter attribute.

$.post('/Webhooks/SQLEndpoint.ashx/socialMedia/970', 'myTwitterAccount');

For this request, the @AttributeId will be 970 and the @Body will be myTwitterAccount.

So we just showed how you can update a single attribute at a time. What if we wanted to update all three with a single request? If you are running SQL Server 2016+ or Azure SQL then you can do just that.


IF EXISTS (SELECT * FROM [AttributeValue] WHERE AttributeId = 969 AND EntityId = @PersonId)
    UPDATE [AttributeValue]
        SET [Value] = JSON_VALUE(@Body, '$.Twitter')
        WHERE [AttributeId] = 969 AND [EntityId] = @PersonId
    INSERT INTO [AttributeValue]
        ([IsSystem], [AttributeId], [EntityId], [Value], [Guid])
        VALUES (0, 969, @PersonId, JSON_VALUE(@Body, '$.Facebook'), NEWID())

IF EXISTS (SELECT * FROM [AttributeValue] WHERE AttributeId = 970 AND EntityId = @PersonId)
    UPDATE [AttributeValue]
        SET [Value] = JSON_VALUE(@Body, '$.Facebook')
        WHERE [AttributeId] = 970 AND [EntityId] = @PersonId
    INSERT INTO [AttributeValue]
        ([IsSystem], [AttributeId], [EntityId], [Value], [Guid])
        VALUES (0, 970, @PersonId, JSON_VALUE(@Body, '$.Twitter'), NEWID())

IF EXISTS (SELECT * FROM [AttributeValue] WHERE AttributeId = 969 AND EntityId = @PersonId)
    UPDATE [AttributeValue]
        SET [Value] = JSON_VALUE(@Body, '$.Twitter')
        WHERE [AttributeId] = 971 AND [EntityId] = @PersonId
    INSERT INTO [AttributeValue]
        ([IsSystem], [AttributeId], [EntityId], [Value], [Guid])
        VALUES (0, 971, @PersonId, JSON_VALUE(@Body, '$.Twitter'), NEWID())

So what we have done is taken the guts of the previous example and duplicated it a few times. Then we modified each one to be for a hard-coded attribute. Notice we are now using a statement such as JSON_VALUE(@Body, '$.Twitter') to get our value. This is a new function available in SQL Server 2016+ and Azure SQL. It lets us process a JSON encoded string (@Body) and extract a single value from the tree path. The path can be pretty complex, so go read up on it and it's related functions!

$.post('/Webhooks/SQLEndpoint.ashx/socialMedia', JSON.stringify({Twitter: 'tw', Facebook: 'fb', Instagram: 'ig'}));

The above creates a JSON object and then converts it to a string format. This JSON-encoded string will be passed to our SQL statement as the @Body parameter. Which, as you saw above, we can then parse on the fly. If we really wanted to be fancy above we could have included various checks to make sure the JSON values existed before making any changes, but this is a simplistic example.