Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/soql-model/src/model/impl/fieldSelectionImpl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ describe('FieldSelectionImpl should', () => {
expect(actual).toEqual(expected);
});
it('store an unmodeled syntax object as the alias', () => {
const expected = { field: { fieldName: 'brian' }, alias: { unmodeledSyntax: 'bill' } };
const expected = { field: { fieldName: 'brian' }, alias: { unmodeledSyntax: 'bill', reason: 'unmodeled:alias' } };
const actual = new Impl.FieldSelectionImpl(
new Impl.FieldRefImpl(expected.field.fieldName),
new Impl.UnmodeledSyntaxImpl(expected.alias.unmodeledSyntax)
new Impl.UnmodeledSyntaxImpl(expected.alias.unmodeledSyntax, 'unmodeled:alias')
);
expect(actual).toEqual(expected);
});
it('return field name followed by alias for toSoqlSyntax()', () => {
const expected = 'rolling stones';
const actual = new Impl.FieldSelectionImpl(
new Impl.FieldRefImpl('rolling'),
new Impl.UnmodeledSyntaxImpl('stones')
new Impl.UnmodeledSyntaxImpl('stones', 'unmodeled:alias')
).toSoqlSyntax();
expect(actual).toEqual(expected);
});
Expand Down
12 changes: 6 additions & 6 deletions packages/soql-model/src/model/impl/fromImpl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,22 @@ describe('FromImpl should', () => {
it('store as and using clauses as unmodeled syntax', () => {
const expected = {
sobjectName: 'black',
as: { unmodeledSyntax: 'and' },
using: { unmodeledSyntax: 'blue' },
as: { unmodeledSyntax: 'and', reason: 'unmodeled:as' },
using: { unmodeledSyntax: 'blue', reason: 'unmodeled:using' },
};
const actual = new Impl.FromImpl(
expected.sobjectName,
new Impl.UnmodeledSyntaxImpl(expected.as.unmodeledSyntax),
new Impl.UnmodeledSyntaxImpl(expected.using.unmodeledSyntax)
new Impl.UnmodeledSyntaxImpl(expected.as.unmodeledSyntax, 'unmodeled:as'),
new Impl.UnmodeledSyntaxImpl(expected.using.unmodeledSyntax, 'unmodeled:using')
);
expect(actual).toEqual(expected);
});
it('return FROM sobject name followed by as and using clauses for toSoqlSyntax()', () => {
const expected = 'FROM exile on main';
const actual = new Impl.FromImpl(
'exile',
new Impl.UnmodeledSyntaxImpl('on'),
new Impl.UnmodeledSyntaxImpl('main')
new Impl.UnmodeledSyntaxImpl('on', 'unmodeled:as'),
new Impl.UnmodeledSyntaxImpl('main', 'unmodeled:using')
).toSoqlSyntax();
expect(actual).toEqual(expected);
});
Expand Down
24 changes: 12 additions & 12 deletions packages/soql-model/src/model/impl/queryImpl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ describe('QueryImpl should', () => {
select: { selectExpressions: [] },
from: { sobjectName: 'songs' },
where: { condition: { field: { fieldName: 'paint_it' }, operator: '=', compareValue: { type: 'STRING', value: "'black'" } } },
with: { unmodeledSyntax: 'gimme shelter' },
groupBy: { unmodeledSyntax: 'start me up' },
with: { unmodeledSyntax: 'gimme shelter', reason: 'unmodeled:with' },
groupBy: { unmodeledSyntax: 'start me up', reason: 'unmodeled:group-by' },
orderBy: { orderByExpressions: [{ field: { fieldName: 'angie' } }] },
limit: { limit: 5 },
offset: { unmodeledSyntax: 'wild horses' },
bind: { unmodeledSyntax: 'miss you' },
recordTrackingType: { unmodeledSyntax: 'satisfaction' },
update: { unmodeledSyntax: 'under my thumb' },
offset: { unmodeledSyntax: 'wild horses', reason: 'unmodeled:offset' },
bind: { unmodeledSyntax: 'miss you', reason: 'unmodeled:bind' },
recordTrackingType: { unmodeledSyntax: 'satisfaction', reason: 'unmodeled:record-tracking' },
update: { unmodeledSyntax: 'under my thumb', reason: 'unmodeled:update' },
};
const actual = new Impl.QueryImpl(
new Impl.SelectExprsImpl([]),
Expand All @@ -31,14 +31,14 @@ describe('QueryImpl should', () => {
CompareOperator.EQ,
new Impl.LiteralImpl(LiteralType.String, expected.where.condition.compareValue.value)
)),
new Impl.UnmodeledSyntaxImpl(expected.with.unmodeledSyntax),
new Impl.UnmodeledSyntaxImpl(expected.groupBy.unmodeledSyntax),
new Impl.UnmodeledSyntaxImpl(expected.with.unmodeledSyntax, 'unmodeled:with'),
new Impl.UnmodeledSyntaxImpl(expected.groupBy.unmodeledSyntax, 'unmodeled:group-by'),
new Impl.OrderByImpl([new Impl.OrderByExpressionImpl(new Impl.FieldRefImpl(expected.orderBy.orderByExpressions[0].field.fieldName))]),
new Impl.LimitImpl(expected.limit.limit),
new Impl.UnmodeledSyntaxImpl(expected.offset.unmodeledSyntax),
new Impl.UnmodeledSyntaxImpl(expected.bind.unmodeledSyntax),
new Impl.UnmodeledSyntaxImpl(expected.recordTrackingType.unmodeledSyntax),
new Impl.UnmodeledSyntaxImpl(expected.update.unmodeledSyntax)
new Impl.UnmodeledSyntaxImpl(expected.offset.unmodeledSyntax, 'unmodeled:offset'),
new Impl.UnmodeledSyntaxImpl(expected.bind.unmodeledSyntax, 'unmodeled:bind'),
new Impl.UnmodeledSyntaxImpl(expected.recordTrackingType.unmodeledSyntax, 'unmodeled:record-tracking'),
new Impl.UnmodeledSyntaxImpl(expected.update.unmodeledSyntax, 'unmodeled:update')
);
expect(actual).toEqual(expected);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as Impl from '.';
import { SyntaxOptions } from '../model';

describe('SoqlModelObjectImpl should', () => {
const testModelObject = new Impl.UnmodeledSyntaxImpl('mick');
const testModelObject = new Impl.UnmodeledSyntaxImpl('mick', 'fake SOQL');
it('use SyntaxOptions that are passed in', () => {
const expectedSyntaxOptions = new SyntaxOptions();
expectedSyntaxOptions.indent = 50;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ import * as Impl from '.';

describe('UnmodeledSyntaxImpl should', () => {
it('store a string as unmodeledSyntax', () => {
const expected = { unmodeledSyntax: 'ronnie' };
const actual = new Impl.UnmodeledSyntaxImpl(expected.unmodeledSyntax);
const expected = { unmodeledSyntax: 'ronnie', reason: 'fake SOQL' };
const actual = new Impl.UnmodeledSyntaxImpl(expected.unmodeledSyntax, 'fake SOQL');
expect(actual).toEqual(expected);
});
it('return stored syntax for toSoqlSyntax()', () => {
const expected = 'keith';
const actual = new Impl.UnmodeledSyntaxImpl(expected).toSoqlSyntax();
const actual = new Impl.UnmodeledSyntaxImpl(expected, 'fake SOQL').toSoqlSyntax();
expect(actual).toEqual(expected);
});
});
4 changes: 1 addition & 3 deletions packages/soql-model/src/model/impl/unmodeledSyntaxImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ import { SoqlModelObjectImpl } from './soqlModelObjectImpl';

export class UnmodeledSyntaxImpl extends SoqlModelObjectImpl
implements Soql.UnmodeledSyntax {
public unmodeledSyntax: string;
public constructor(syntax: string) {
public constructor(public unmodeledSyntax: string, public reason: string) {
super();
this.unmodeledSyntax = syntax;
}
public toSoqlSyntax(options?: Soql.SyntaxOptions): string {
return this.unmodeledSyntax;
Expand Down
2 changes: 2 additions & 0 deletions packages/soql-model/src/model/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface ModelError {
message: string;
lineNumber: number;
charInLine: number;
grammarRule?: string;
}

export enum ErrorType {
Expand Down Expand Up @@ -229,4 +230,5 @@ export interface UnmodeledSyntax
RecordTrackingType,
Update {
unmodeledSyntax: string;
reason: string;
}
38 changes: 36 additions & 2 deletions packages/soql-model/src/model/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const conditionFieldCompare = new Impl.FieldCompareConditionImpl(field, Soql.Com
const conditionLike = new Impl.LikeConditionImpl(field, literal);
const conditionInList = new Impl.InListConditionImpl(field, Soql.InOperator.In, [literal]);
const conditionIncludes = new Impl.IncludesConditionImpl(field, Soql.IncludesOperator.Includes, [literal]);
const conditionUnmodeled = new Impl.UnmodeledSyntaxImpl('A + B > 10');
const conditionUnmodeled = new Impl.UnmodeledSyntaxImpl('A + B > 10', 'unmodeled:calculated-condition');
const conditionAndOr = new Impl.AndOrConditionImpl(conditionFieldCompare, Soql.AndOr.And, conditionLike);
const conditionNested = new Impl.NestedConditionImpl(conditionFieldCompare);
const conditionNot = new Impl.NotConditionImpl(conditionFieldCompare);
Expand All @@ -30,7 +30,7 @@ describe('SoqlModelUtils should', () => {
new Impl.FieldRefImpl(
'field1',
),
new Impl.UnmodeledSyntaxImpl('alias1')
new Impl.UnmodeledSyntaxImpl('alias1', 'unmodeled:alias')
)
]),
new Impl.FromImpl('object1')
Expand Down Expand Up @@ -117,4 +117,38 @@ describe('SoqlModelUtils should', () => {
nonSimpleGroups.forEach(condition => actual &&= !SoqlModelUtils.isSimpleGroup(condition));
expect(actual).toBeTruthy();
});
it('throws from simpleGroupToArray if condition not simple group', () => {
const nonSimpleGroup = new Impl.AndOrConditionImpl(conditionFieldCompare, Soql.AndOr.Or, conditionAndOr);
expect(() => SoqlModelUtils.simpleGroupToArray(nonSimpleGroup)).toThrow();
});
it('returns array and operator from simpleGroupToArray for simple group', () => {
const simpleGroup = new Impl.AndOrConditionImpl(conditionFieldCompare, Soql.AndOr.And, conditionAndOr);
const { conditions, andOr } = SoqlModelUtils.simpleGroupToArray(simpleGroup);
expect(conditions.length).toEqual(3);
expect(andOr).toEqual(Soql.AndOr.And);
});
it('throws from arrayToSimpleGroup if conditions array empty', () => {
const conditions: Soql.Condition[] = [];
expect(() => SoqlModelUtils.arrayToSimpleGroup(conditions)).toThrow();
});
it('throws from arrayToSimpleGroup if >1 condition and operator missing', () => {
const conditions: Soql.Condition[] = [conditionFieldCompare, conditionLike];
expect(() => SoqlModelUtils.arrayToSimpleGroup(conditions)).toThrow();
});
it('returns simple group condition from arrayToSimpleGroup', () => {
const conditions = [conditionFieldCompare, conditionLike, conditionInList];
const andOr = Soql.AndOr.Or;

const expected = new Impl.AndOrConditionImpl(
conditions[0],
andOr,
new Impl.AndOrConditionImpl(
conditions[1],
andOr,
conditions[2]
)
);
const actual = SoqlModelUtils.arrayToSimpleGroup(conditions, andOr);
expect(actual).toEqual(expected);
});
});
33 changes: 33 additions & 0 deletions packages/soql-model/src/model/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,39 @@ export namespace SoqlModelUtils {
return false;
}

export function simpleGroupToArray(condition: Condition): { conditions: Condition[], andOr?: AndOr } {
if (!isSimpleGroup(condition)) {
throw Error('not simple group');
}
condition = stripNesting(condition);
let conditions: Condition[] = [];
let andOr: AndOr | undefined = undefined;
if (condition instanceof Impl.AndOrConditionImpl) {
conditions = conditions.concat(simpleGroupToArray(condition.leftCondition).conditions);
conditions = conditions.concat(simpleGroupToArray(condition.rightCondition).conditions);
andOr = condition.andOr;
} else {
conditions.push(condition);
}
return { conditions, andOr };
}

export function arrayToSimpleGroup(conditions: Condition[], andOr?: AndOr): Condition {
if (conditions.length > 1 && andOr === undefined) {
throw Error('no operator supplied for conditions');
}
if (conditions.length === 0) {
throw Error('no conditions');
}

if (conditions.length === 1) {
return conditions[0];
} else {
const [left, ...rest] = conditions;
return new Impl.AndOrConditionImpl(left as Condition, andOr as AndOr, arrayToSimpleGroup(rest, andOr));
}
}

export function isSimpleGroup(condition: Condition, andOr?: AndOr): boolean {
// a simple group is a condition that can be expressed as an ANY or ALL group of conditions
// ANY: simple conditions all joined by OR
Expand Down
36 changes: 18 additions & 18 deletions packages/soql-model/src/serialization/deserializer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,37 @@ const testQueryModel = {
selectExpressions: [
{ field: { fieldName: 'field1' } },
{ field: { fieldName: 'field2' } },
{ field: { fieldName: 'field3' }, alias: { unmodeledSyntax: 'alias3' } },
{ unmodeledSyntax: 'COUNT(fieldZ)' },
{ unmodeledSyntax: '(SELECT fieldA FROM objectA)' },
{ unmodeledSyntax: 'TYPEOF obj WHEN typeX THEN fieldX ELSE fieldY END' },
{ field: { fieldName: 'field3' }, alias: { unmodeledSyntax: 'alias3', reason: 'unmodeled:alias' } },
{ unmodeledSyntax: 'COUNT(fieldZ)', reason: 'unmodeled:function-reference' },
{ unmodeledSyntax: '(SELECT fieldA FROM objectA)', reason: 'unmodeled:semi-join' },
{ unmodeledSyntax: 'TYPEOF obj WHEN typeX THEN fieldX ELSE fieldY END', reason: 'unmodeled:type-of' },
],
},
from: { sobjectName: 'object1' },
where: { condition: { field: { fieldName: 'field1' }, operator: '=', compareValue: { type: 'NUMBER', value: '5' } } },
with: { unmodeledSyntax: 'WITH DATA CATEGORY cat__c AT val__c' },
groupBy: { unmodeledSyntax: 'GROUP BY field1' },
with: { unmodeledSyntax: 'WITH DATA CATEGORY cat__c AT val__c', reason: 'unmodeled:with' },
groupBy: { unmodeledSyntax: 'GROUP BY field1', reason: 'unmodeled:group-by' },
orderBy: {
orderByExpressions: [
{ field: { fieldName: 'field2' }, order: 'DESC', nullsOrder: 'NULLS LAST' },
{ field: { fieldName: 'field1' } }
]
},
limit: { limit: 20 },
offset: { unmodeledSyntax: 'OFFSET 2' },
bind: { unmodeledSyntax: 'BIND field1 = 5' },
recordTrackingType: { unmodeledSyntax: 'FOR VIEW' },
update: { unmodeledSyntax: 'UPDATE TRACKING' },
offset: { unmodeledSyntax: 'OFFSET 2', reason: 'unmodeled:offset' },
bind: { unmodeledSyntax: 'BIND field1 = 5', reason: 'unmodeled:bind' },
recordTrackingType: { unmodeledSyntax: 'FOR VIEW', reason: 'unmodeled:record-tracking' },
update: { unmodeledSyntax: 'UPDATE TRACKING', reason: 'unmodeled:update' },
errors: [],
};

const fromWithUnmodeledSyntax = {
sobjectName: 'object1',
as: { unmodeledSyntax: 'AS objectAs' },
using: { unmodeledSyntax: 'USING SCOPE everything' },
as: { unmodeledSyntax: 'AS objectAs', reason: 'unmodeled:as' },
using: { unmodeledSyntax: 'USING SCOPE everything', reason: 'unmodeled:using' },
};

const selectCountUnmdeledSyntax = { unmodeledSyntax: 'SELECT COUNT()' };
const selectCountUnmdeledSyntax = { unmodeledSyntax: 'SELECT COUNT()', reason: 'unmodeled:count' };

const limitZero = { limit: 0 };

Expand All @@ -59,16 +59,16 @@ const field = { fieldName: 'field' };

const conditionFieldCompare = { field, operator: '=', compareValue: literalNumber };
const conditionLike = { field, compareValue: literalString };
const conditionInList = { unmodeledSyntax: "field IN ( 'HelloWorld', 'other value' )" }
const conditionIncludes = { unmodeledSyntax: "field INCLUDES ( 'HelloWorld', 'other value' )" }
const conditionInList = { unmodeledSyntax: "field IN ( 'HelloWorld', 'other value' )", reason: 'unmodeled:in-list-condition' }
const conditionIncludes = { unmodeledSyntax: "field INCLUDES ( 'HelloWorld', 'other value' )", reason: 'unmodeled:includes-condition' }
// uncomment when in-list conditions are supported----const conditionInList = { field, operator: 'IN', values: [literalString, { ...literalString, value: "'other value'" }] };
// uncomment when includes conditions are supported---const conditionIncludes = { field, operator: 'INCLUDES', values: [literalString, { ...literalString, value: "'other value'" }] };
const conditionAndOr = { leftCondition: conditionFieldCompare, andOr: 'AND', rightCondition: conditionLike };
const conditionNested = { condition: conditionFieldCompare };
const conditionNot = { condition: conditionFieldCompare };
const conditionCalculated = { unmodeledSyntax: 'A + B > 10' };
const conditionDistance = { unmodeledSyntax: "DISTANCE(field,GEOLOCATION(37,122),'mi') < 100" };
const conditionSemiJoin = { unmodeledSyntax: 'field IN (SELECT A FROM B)' };
const conditionCalculated = { unmodeledSyntax: 'A + B > 10', reason: 'unmodeled:calculated-condition' };
const conditionDistance = { unmodeledSyntax: "DISTANCE(field,GEOLOCATION(37,122),'mi') < 100", reason: 'unmodeled:distance-condition' };
const conditionSemiJoin = { unmodeledSyntax: 'field IN (SELECT A FROM B)', reason: 'unmodeled:in-semi-join-condition' };

describe('ModelDeserializer should', () => {
it('model supported syntax as query objects', () => {
Expand Down
Loading