Shared query template processing for MemberJunction data providers. Handles parameter validation, type conversion, and Nunjucks template rendering for parameterized queries.
MemberJunction's RunQuery system supports parameterized SQL queries written in the MJ Query language, a defined subset of Nunjucks over SQL (e.g., {{ status | sqlString }}). This package provides the QueryParameterProcessor class that the database providers use to:
required, email, min, max, trim, upper, lower, number, date, sqlsafe, sqljoin)Context object) out of the caller's reachIt does not check the template's syntax. Which tags, expressions, filters and Context tokens a query may use is defined by the MJ Query language. Those rules are checked when a query is saved (the reference implementation is QueryTemplate in @memberjunction/sql-parser), and the RunQuery pipeline in @memberjunction/generic-database-provider checks a template again before it renders. This processor renders whatever template it is given.
It does not decide whether SQL is a template. ProcessQueryTemplate renders when the query's UsesTemplate is true or when the caller passes forceTemplateProcessing; otherwise it returns the SQL unchanged. The RunQuery pipeline decides from the SQL itself: a query whose SQL holds template syntax outside SQL comments is rendered even when its stored UsesTemplate flag is false, because unrendered {{ }} text is never valid SQL.
Each render names the dialect its SQL is written in, through QueryRenderOptions.DialectKey. The key is any key @memberjunction/sql-dialect resolves: 'sqlserver', 'postgresql', or the dialect of an External Data Source such as 'mysql' or 'snowflake'. The RunQuery pipeline passes the dialect of the request: the provider's own platform, or the External Data Source's dialect for a query that runs on one. Two providers on different platforms in one process therefore each render with their own filters.
The dialect decides:
| Feature | SQL Server | PostgreSQL | Comes from |
|---|---|---|---|
| Boolean parameter values | 1 / 0 (BIT) |
true / false |
The dialect's Binding.BooleanParameterValue |
sqlBoolean filter |
1 / 0 |
true / false |
Writer.BooleanLiteral |
sqlString / sqlIn filters |
N'…' for text outside ASCII, otherwise '…' |
'…' |
Writer.StringLiteralPrefix, Writer.QuoteStringLiteral |
sqlIdentifier filter |
[name] |
"name" |
Writer.QuoteIdentifier |
sqlLikeContains / sqlLikeBegins / sqlLikeEnds filters |
Escapes %, _ and [ as [%], [_], [[] |
Escapes %, _ and \ with a backslash |
Writer.EscapeLikePattern |
A dialect with no Binding facet (the query-only dialects) keeps a boolean parameter as a JavaScript boolean. An unknown dialect key is an error: ValidateParameters throws, and ProcessQueryTemplate returns a failed result.
The processor keeps one Nunjucks environment per dialect key, built the first time that key is rendered, since filters are baked in when an environment is created. The random built-in filter is replaced with one that refuses to render, because a query must render the same SQL every time.
When no DialectKey is given, the processor falls back to the process-wide default platform, RunQuerySQLFilterManager.Instance.Platform in @memberjunction/core. That is only right for a process that talks to one kind of database. RunQuerySQLFilterManager.SetPlatform still sets that default but is deprecated: pass the dialect on each render instead.
null and undefined count as no value. A parameter the caller passes as null or undefined is treated exactly like one the caller left out: it is left out of the validated values, a required parameter reports it as missing, and a template's default(...) filter applies to it. An empty string also fails a required check.DefaultValue is informational only and is never injected. Write the default in the SQL, with {{ name | default(...) }} or an {% if %} / {% else %} block. The environment is created with throwOnUndefined, so a template that reads a missing value without a default fails to render.IsServerSet, for a name in QueryServerValues.ProtectedNames, or for the reserved name Context is refused and logged. Server-set parameters take their values from QueryServerValues.ServerParameters only, and those values are left out of appliedParameters.forceTemplateProcessing is passed (the outer query of a composition need not define its dependencies' parameters).import { QueryParameterProcessor } from '@memberjunction/query-processor';
import type { QueryRenderOptions, QueryTemplateInput } from '@memberjunction/query-processor';
const query: QueryTemplateInput = {
SQL: 'SELECT ID, Name FROM Customers WHERE Status = {{ status | sqlString }} AND Active = {{ active | default(true) | sqlBoolean }}',
UsesTemplate: true,
Parameters: [
{ Name: 'status', Type: 'string', IsRequired: true, IsServerSet: false, ValidationFilters: null },
{ Name: 'active', Type: 'boolean', IsRequired: false, IsServerSet: false, ValidationFilters: null },
],
};
const options: QueryRenderOptions = { DialectKey: 'postgresql' };
// Validate parameters on their own
const validation = QueryParameterProcessor.ValidateParameters({ status: 'Active' }, query.Parameters, false, undefined, options);
// Or validate and render in one step
const result = QueryParameterProcessor.ProcessQueryTemplate(
query,
{ status: 'Active', active: null }, // null counts as no value, so default(true) applies
undefined, // sqlOverride: SQL to render instead of query.SQL
false, // forceTemplateProcessing
undefined, // serverValues: ServerParameters, ProtectedNames, Context
options
);
if (result.success) {
// result.processedSQL:
// SELECT ID, Name FROM Customers WHERE Status = 'Active' AND Active = true
}
ProcessQueryTemplate accepts a full QueryInfo (saved queries) or any object of the QueryTemplateInput shape (transient specs). The lower-case validateParameters and processQueryTemplate are deprecated aliases with the same parameters.
QueryRenderOptions.Scope adds names to one render only, such as a callback the template calls. They win over parameter values, Context wins over them, and nothing is kept between renders.
| sqlString)Query templates have access to the server-populated Context object (Context.UserID, Context.UserEmail, Context.UserName, Context.Now, Context.Today, Context.StartOfWeek, Context.StartOfMonth, Context.StartOfQuarter, Context.StartOfYear), passed as QueryServerValues.Context. The Context members a template read come back in QueryProcessingResult.contextValues, since a cached result depends on exactly those.
Warning:
Context.UserEmailandContext.UserNameresolve to raw, unescaped strings (for example,O'Brien) and templates render withautoescape: false. Always pipe user string tokens through| sqlStringin SQL templates — never interpolate them bare:-- Correct (sqlString wraps in single quotes and doubles embedded apostrophes): WHERE Email = {{ Context.UserEmail | sqlString }} AND DisplayName = {{ Context.UserName | sqlString }} -- WRONG (breaks on apostrophes like O'Brien and allows SQL injection): WHERE Email = '{{ Context.UserEmail }}'
@memberjunction/core - For RunQuerySQLFilterManager (the per-dialect SQL filters and dialect lookup), QUERY_CONTEXT_NAME, QueryTemplateContext and LogError@memberjunction/core-entities - For the MJQueryParameterEntity fields a parameter definition reads@memberjunction/global - MJ's shared global utilities (declared; the processor's own source does not import it)nunjucks - Template rendering engine