Open source implementation of the SOQL.
You can query everything you want by defining the resolvers.
SOQL is an object-oriented query language that allows you to query related data based on an object graph.
npm install open-soql
NOTICE:
Use withwebpack >= 5
If you get the error:
Module not found: Error: Can't resolve '(importing/path/to/filename)' in '(path/to/node_modules/path/to/dirname)' Did you mean '(filename).js'?`
Add following setting to your
webpack.config.js
.{ test: /\.m?js/, resolve: { fullySpecified: false, }, },On
webpack >= 5
, the extension in the request is mandatory for it to be fully specified if the origin is a '.mjs' file or a '.js' file where the package.json contains '"type": "module"'.
import { build } from 'open-soql/modules/builder';
import { staticJsonResolverBuilder,
staticCsvResolverBuilder,
passThroughResolverBuilder } from 'open-soql/modules/resolvers';
// See `src/types.ts` > `QueryBuilderInfo`
const { compile, soql,
insert, update, remove, touch, notifyRemoved,
transaction,
subscribe, unsubscribe, unsubscribeAllBySubscriber } = build({
functions: [{ // optional: For defining custom functions.
type: 'scalar',
name: 'string',
fn: (ctx, args, records) => {
return String(args[0]);
},
}, {
type: 'scalar',
name: 'number',
fn: (ctx, args, records) => {
return Number(args[0]);
},
}, {
type: 'immediate-scalar',
name: 'cast_string',
fn: (ctx, args) => {
return String(args[0]);
},
}, {
type: 'immediate-scalar',
name: 'cast_number',
fn: (ctx, args) => {
return Number(args[0]);
},
}, {
type: 'aggregate',
name: 'count_twice',
fn: (ctx, args, records) => {
return records.length * 2;
},
}],
events: { // optional: For resolving transaction and N+1 query problem.
beginTransaction: (evt) => Promise.resolve(),
endTransaction: (evt, err) => Promise.resolve(),
beginExecute: (evt) => Promise.resolve(),
endExecute: (evt, err) => Promise.resolve(),
beforeMasterSubQueries: (evt) => Promise.resolve(),
afterMasterSubQueries: (evt) => Promise.resolve(),
beforeDetailSubQueries: (evt) => Promise.resolve(),
afterDetailSubQueries: (evt) => Promise.resolve(),
},
resolvers: {
query: {
Account: (fields, conditions, limit, offset, ctx) => {
// Fetch the `Account` object data.
ctx.resolverCapabilities.filtering = true; // True if the resolver can filter records.
return Promise.resolve([{ ... }, ... ]);
},
Contact: (fields, conditions, limit, offset, ctx) => {
// Fetch the `Contact` object data.
// `ctx.parent` is a parent record.
ctx.resolverCapabilities.filtering = true; // True if the resolver can filter records.
return Promise.resolve([{ ... }, ... ]);
},
Opportunity: (fields, conditions, limit, offset, ctx) => {
// Fetch the `Opportunity` object data.
// `ctx.parent` is a parent record.
ctx.resolverCapabilities.filtering = true; // True if the resolver can filter records.
return Promise.resolve([{ ... }, ... ]);
},
Event: staticCsvResolverBuilder( // (CSV string)
// "staticJsonResolverBuilder"(JSON string) and
// "passThroughResolverBuilder"(array of object)
// are also available.
'Event', () => Promise.resolve(`
Id, Subject, WhatId
Event/1, Email, Account/1
Event/2, Phone, Contact/1
`)
),
},
insert: { // optional: For DML
Contact: (records, ctx) => {
return Promise.resolve(records.map((x, i) => ({...x, id: `Contact/${i}`})));
},
},
update: { // optional: For DML
Contact: (records, ctx) => {
return Promise.resolve(records);
},
},
remove: { // optional: For DML
Contact: (records, ctx) => {
return Promise.resolve();
},
},
},
relationships: { // optional: For relationship query
/**
* detailResolverName
* e.g.: Contact: { account: 'Account' }
* Contact: { account: { resolver: 'Account', id: 'accountId' } }
*
* NOTE: 'Account' is `masterResolverName`.
* 'account' is `masterObjectFieldName`.
* 'accountId' is `masterIdName`. (foreign key field name)
* `Contact (resolver) -> account (field name)` direction is `Detail to Master`.
*
* masterResolverName
* e.g.: Account: { contacts: ['Contact'] }
* Account: { contacts: ['Contact', 'account'] }
*
* NOTE: 'contacts' is details relationship name.
* 'Contact' is `detailResolverName` and 'account' is Contact's `masterObjectFieldName`.
* Default masterObjectFieldName is `MasterResolverName`.
* `Account (resolver) -> contacts (relationship name)` direction is `Master to Details`.
*/
Account: {
Contacts: ['Contact'], // master->details relationship
Opportunities: ['Opportunity', 'Account'], // master->details relationship
}, // (Explicitly specify relationship item)
Contact: {
Account: 'Account', // detail->master relationship
},
Opportunity: {
Account: 'Account', // detail->master relationship
},
Event: {
Account: { resolver: 'Account', id: 'WhatId' }, // detail->master relationship
Contact: { resolver: 'Contact', id: 'WhatId' }, // (Explicitly specify Id item)
Opportunity: { resolver: 'Opportunity', id: 'WhatId' },
},
},
});
const result = await soql<Partial<Contact>>`
Select
acc.id aid
, acc.Region reg
, acc.Category cat
, (
Select id, Name
from acc.Opportunities
where Amount > ${10000}
-- It can be number, string, boolean or null.
order by DueDate desc limit 5
)
, string(id)
, string(foo)
, string(reg)
, string(acc.qux)
from Contact con, con.Account acc
where
(
number(acc.numOfEmployees) = 5
and acc.created > ${{type: 'date', value: '2020-01-01'}}
-- It can be 'date' or 'datetime'.
and acc.updated > 2020-01-01
) or (
acc.foo = 1
and acc.bar = 2
and acc.baz = 2
) or not (
acc.qux = 1
and acc.quux = 2
and acc.corge in (Select id from Event)
)
order by aid, reg, cat
limit 10 offset 2
-- line comment
/* block comment */
`;
// result is [{...}, ...]
- Non-parameterized query.
(Template literal parameters will be interpreted before compiling.)
const query = compile`Select id from account where id > ${'100'}`;
const result = await query.execute<Partial<Account>>();
- Named parameterized query.
const query = compile`Select id from account where id > :idGreaterThan`;
const result = await query.execute<Partial<Account>>({ idGreaterThan: '100' });
You can use parameters on the right side of the conditional expression, function arguments, limit, and offset.
const aggregationResult = await soql<ContactAgg>`
Select
count()
, count(id) cnt
, sum(bar) sum
, cast_string(12345) str
, cast_number('2234') num
from
Contact
where
foo > ''
group by Region
having count(id) > 0
`;
// aggregationResult is [{...}, ...]
const inserted = await insert('Contact', [{
Name: 'foo',
}]);
// inserted is [{ Id: 'Contact/1', Name: 'foo' }]
const updated = await update('Contact', inserted);
// updated is [{ Id: 'Contact/1', Name: 'foo' }]
await remove('Contact', updated);
const selected = await soql<Partial<Contact>>`Select Id, Name from Contact`;
const updated2 = await update('Contact', selected);
const inserted = await insert('Contact', {
Name: 'foo',
});
// inserted is { Id: 'Contact/1', Name: 'foo' }
const updated = await update('Contact', inserted);
// updated is { Id: 'Contact/1', Name: 'foo' }
await remove('Contact', updated);
await transaction(async (commands, tr) => {
const { compile, soql, insert, update, remove, touch, notifyRemoved } = commands;
const inserted = await insert('Contact', [{
Name: 'foo',
}]);
const selected = await soql<Partial<Contact>>`Select Id, Name from Contact`;
const updated = await update('Contact', selected);
await remove('Contact', updated);
const query = compile`Select id from account where id > ${'100'}`;
const selectedAccounts = await query.execute<Partial<Account>>();
});
const subscriber: Subscriber = ({on, resolver, id}) => {
switch (on) {
case 'insert':
...
break;
case 'update':
...
break;
case 'remove':
...
break;
}
};
// Subscribe to all changes of the resolver `Contact`.
subscribe('Contact', null, subscriber);
// Subscribe to all changes of the record `Contact(id='Contact/z2')`.
subscribe('Contact', 'Contact/z2', subscriber);
await update('Contact', [ ... ]); // or insert(), remove(), touch()
// (Fire events on next event loop.)
await update('Contact', [ ... ]);
// (Fire events on next event loop.)
await update('Contact', [ ... ]);
// (Fire events on next event loop.)
...
// Unsubscribe to all changes of the resolver `Contact`.
unsubscribe('Contact', null, subscriber);
// Unsubscribe to all changes of the record `Contact(id='Contact/z2')`.
unsubscribe('Contact', 'Contact/z2', subscriber);
const subscriber: Subscriber = ({on, resolver, id}) => { ... };
// Subscribe to all changes of the resolver `Contact`.
subscribe('Contact', null, subscriber);
// Subscribe to all changes of the record `Contact(id='Contact/z2')`.
subscribe('Contact', 'Contact/z2', subscriber);
await transaction(async (commands, tr) => {
const { compile, soql, insert, update, remove, touch } = commands;
await update('Contact', [ ... ]); // or insert(), remove(), touch()
await update('Contact', [ ... ]);
await update('Contact', [ ... ]);
});
// (Fire events on next event loop.)
...
// Unsubscribe to all changes of the resolver `Contact`.
unsubscribe('Contact', null, subscriber);
// Unsubscribe to all changes of the record `Contact(id='Contact/z2')`.
unsubscribe('Contact', 'Contact/z2', subscriber);
See also the following usage example repositories:
Select
field list- detail-master relationship name
- resolver (relationship) alias name
- field alias name
- function call (aggregate | scalar | immediate_scalar)
- nested function call (call functions in actual parameters of functions)
- functions
- Aggregate functions
-
count()
,count(field)
-
count_distinct(field)
-
sum(field)
-
avg(field)
-
min(field)
-
max(field)
-
grouping(field)
-
- Scalar functions
- String functions
-
format(field | literal | function call)
-
concat(field | literal | function call, ...)
-
- Cast functions
-
cast_to_string(field | literal | function call)
-
cast_to_number(field | literal | function call)
-
cast_to_boolean(field | literal | function call)
-
- Calc functions
-
add(field | literal | function call, ...)
-
sub(field | literal | function call, ...)
-
mul(field | literal | function call, ...)
-
div(field | literal | function call, ...)
-
mod(field | literal | function call, ...)
-
- Date and datetime functions (UTC)
-
convertTimezone(field | literal | function call)
-
calendar_month(field | literal | convertTimezone(field) | function call)
-
calendar_quarter(field | literal | convertTimezone(field) | function call)
-
calendar_year(field | literal | convertTimezone(field) | function call)
-
day_in_month(field | literal | convertTimezone(field) | function call)
-
day_in_week(field | literal | convertTimezone(field) | function call)
-
day_in_year(field | literal | convertTimezone(field) | function call)
-
day_only(field | literal | convertTimezone(field) | function call)
-
fiscal_month(field | literal | convertTimezone(field) | function call)
-
fiscal_quarter(field | literal | convertTimezone(field) | function call)
-
fiscal_year(field | literal | convertTimezone(field) | function call)
-
hour_in_day(field | literal | convertTimezone(field) | function call)
-
week_in_month(field | literal | convertTimezone(field) | function call)
-
week_in_year(field | literal | convertTimezone(field) | function call)
-
- Date and datetime functions (local timezone)
-
calendar_month_lc(field | literal | function call)
-
calendar_quarter_lc(field | literal | function call)
-
calendar_year_lc(field | literal | function call)
-
day_in_month_lc(field | literal | function call)
-
day_in_week_lc(field | literal | function call)
-
day_in_year_lc(field | literal | function call)
-
day_only_lc(field | literal | function call)
-
fiscal_month_lc(field | literal | function call)
-
fiscal_quarter_lc(field | literal | function call)
-
fiscal_year_lc(field | literal | function call)
-
hour_in_day_lc(field | literal | function call)
-
week_in_month_lc(field | literal | function call)
-
week_in_year_lc(field | literal | function call)
-
- String functions
- Aggregate functions
-
TYPEOF
expression
- field expressions
- field
- field alias name
- data types
- string
- number
- date
- datetime
- null
From
clause- resolver (relationship name) alias
Where
clause- field
- data types
- string
- number
- date
- datetime
- null
- op1 function call (scalar | immediate_scalar)
- op2 function call (immediate_scalar)
- date literals (e.g.:
TODAY
) - logical operators
-
and
-
or
-
not
-
- comparison operators
-
=
-
!=
-
<
-
<=
-
>
-
>=
-
like
-
not_like
-
in
-
not_in
-
includes
-
excludes
-
Having
clause- field
- data types
- string
- number
- date
- datetime
- null
- op1 function call (immediate_scalar | aggregate)
- op2 function call (immediate_scalar)
- date literals (e.g.:
TODAY
) - logical operators
-
and
-
or
-
not
-
- comparison operators
-
=
-
!=
-
<
-
<=
-
>
-
>=
-
like
-
not_like
-
in
-
not_in
-
includes
-
excludes
-
Group by
clause- fields
- field alias name
-
ROLLUP
-
CUBE
Order by
clause- fields
- resolver (relationship) alias name
- field alias name
- asc/desc
- nulls first/last
-
Using scope
clause -
Limit
clause -
Offset
clause -
With
clause -
For
clause
- prepared query (pre-compiled query)
- named parameterized query
- standard query resolvers
- JSON string
- CSV string
- Array of object
- DML
-
insert
-
update
-
remove
-
- Publish / Subscribe messaging
- transaction scope
- template string
export interface QueryBuilderInfo {
functions?: QueryFuncInfo[];
// QueryFuncInfo[i].type is 'aggregate' | 'scalar' | 'immediate-scalar'
rules?: {
idFieldName?: (resolverName: string) => string;
foreignIdFieldName?: (masterResolverName: string | undefined) => string | undefined;
};
events?: {
beginTransaction?: (evt: ResolverEvent) => Promise<void>;
endTransaction?: (evt: ResolverEvent, err: Error | null) => Promise<void>;
beginExecute?: (evt: ResolverEvent) => Promise<void>;
endExecute?: (evt: ResolverEvent, err: Error | null) => Promise<void>;
beforeMasterSubQueries?: (evt: ResolverEvent) => Promise<void>;
afterMasterSubQueries?: (evt: ResolverEvent) => Promise<void>;
beforeDetailSubQueries?: (evt: ResolverEvent) => Promise<void>;
afterDetailSubQueries?: (evt: ResolverEvent) => Promise<void>;
};
resolvers: {
query: {
[resolverNames: string]: QueryResolverFn;
};
insert?: {
[resolverNames: string]: InsertResolverFn;
};
update?: {
[resolverNames: string]: UpdateResolverFn;
};
remove?: {
[resolverNames: string]: RemoveResolverFn;
};
};
relationships?: {
[detailOrMasterResolverNames: string]: {
[fieldOrRelNames: string]:
string | { resolver: string, id: string } | [string, string?];
};
};
}
export interface IQuery {
public execute<R>(
params?: {
[paramNames: string]:
number | string | null |
Array<number | string | null>
}): Promise<R[]>;
}
export interface SubscriberParams {
on: 'insert' | 'update' | 'remove';
resolver: string;
id: any | null;
}
export type Subscriber = (params: SubscriberParams) => void;
export function build(builder: QueryBuilderInfo): {
compile: (strings: TemplateStringsArray | string,
...values: Array<PreparedAtomValue | Array<PreparedAtomValue>>) => IQuery;
soql: (strings: TemplateStringsArray | string,
...values: Array<PreparedAtomValue | Array<PreparedAtomValue>>) => Promise<R[]>;
insert: (resolver: string, obj: T) => Promise<T extends (infer R)[] ? R[] : T>;
update: (resolver: string, obj: T) => Promise<T extends (infer R)[] ? R[] : T>;
remove: (resolver: string, obj: T) => Promise<void>;
touch: (resolver: string, obj: T) => Promise<void>;
notifyRemoved: (resolver: string, obj: T) => Promise<void>;
subscribe: (resolver: string, id: any, fn: Subscriber) => void,
unsubscribe: (resolver: string, id: any, fn: Subscriber) => void,
unsubscribeAllBySubscriber: (resolver: string, fn: Subscriber) => void,
transaction: (
callback: (commands: {
compile, soql, insert, update, remove, touch
}, tr: any) => Primise<void>,
trOptions?: any,
) => Primise<void>;
};
- Set up the resolvers.
builder
: Resolvers and configurations.
NOTICE:
Theimmediate-scalar
function does not refer to the fields of a record and must be referentially transparent.
NOTICE:
If query conditions includes computed fields,QueryResolverFn
's parameterconditions
will be[]
.
To get complete conditions, use parameterctx.conditions
.
You can get transformed conditions that include only the fields you specified by usinggetIndexFieldConditions()
.
- Functions that execute select queries and DML
compile
: Compile the query.soql
: Select records.insert
: Insert record(s).update
: Update record(s).remove
: Remove record(s).touch
: Queuesupdate
events for subscribers. (to notify remote changes)notifyRemoved
: Queuesremove
events for subscribers. (to notify remote changes)subscribe
: Subscribe to publishing events.unsubscribe
: Unsubscribe to publishing events.unsubscribeAllBySubscriber
: Unsubscribe to publishing events.transaction
: Execute commands within a transaction.
export function sortRecords(query: PreparedQuery, records: any[]): any[];
- Sort records.
query
: Prepared query object.records
: Records to sort.
- Sorted records.
export function applyWhereConditions(
Omit<ResolverContext, 'resolverCapabilities'>,
conds: PreparedCondition[], records: any[]): any[];
- Filter records by
where
conditions.
ctx
: Context object.conds
:where
conditions.records
: Records to apply the filter.
- Records that the filter applied.
export function applyHavingConditions(
Omit<ResolverContext, 'resolverCapabilities'>,
conds: PreparedCondition[], groupedRecsArray: any[][]): any[];
- Filter groups by
having
conditions.
ctx
: Context object.conds
:having
conditions.records
: Groups to apply the filter.
- Groups that the filter applied.
export function getIndexFieldConditions(
ctx: Pick<ResolverContext, 'params'>,
conds: PreparedCondition[], indexFieldNames: string[]): PreparedCondition[];
- Gets the transformed conditions that include only the fields specified in
indexFieldNames
.
ctx
: Context object.conds
: Original conditions.indexFieldNames
: Index fields.
- Transformed conditions.
export interface SqlDialect {
fieldName: (name: string) => string;
escapeString: (s: string) => string;
}
export function getSqlConditionString(
ctx: Pick<ResolverContext, 'params'>,
conds: PreparedCondition[], dialect: SqlDialect): string;
- Get the SQL condition string.
ctx
: Context object.conds
: Conditions for converting to SQL conditions.dialect
: SQL dialect.
- SQL condition string (where clause excludes the
where
keyword).
export function escapeSqlStringLiteral_Std(s: string): string;
- Escape the standard SQL string literal. (pass to
SqlDialect
)
s
: string literal.
- Escaped string.
export function escapeSqlStringLiteral_MySql(s: string): string;
- Escape the MySQL string literal. (pass to
SqlDialect
)
s
: string literal.
- Escaped string.
export interface StaticResolverConfig {
noCache?: boolean;
noFiltering?: boolean;
noSorting?: boolean;
}
export const staticJsonResolverBuilder:
(resolverName: string, fetcher: () => Promise<string>,
config?: StaticResolverConfig) => QueryResolverFn;
- Generate the query resolver for static JSON data.
resolverName
: Resolver name.fetcher
: The function that returns promise of data.
- Query resolver.
export const staticCsvResolverBuilder:
(resolverName: string, fetcher: () => Promise<string>,
config?: StaticResolverConfig) => QueryResolverFn;
- Generate the query resolver for static CSV data.
resolverName
: Resolver name.fetcher
: The function that returns promise of data.
- Query resolver.
export const passThroughResolverBuilder:
(resolverName: string, fetcher: () => Promise<any[]>,
config?: StaticResolverConfig) => QueryResolverFn;
- Generate the query resolver for static object array data.
resolverName
: Resolver name.fetcher
: The function that returns promise of data.
- Query resolver.
- What does
SOQL
stand for?- π In
Open SOQL
,SOQL
stands forSOQL is Object Query Language
. - π In original SOQL,
SOQL
stands forSalesforce Object Query Language
.
- π In
ISC
Copyright (c) 2020 Shellyl_N and Authors