@linked-db/linked-ql
Version:
A query client that extends standard SQL with new syntax sugars and enables auto-versioning capabilities on any database
471 lines (447 loc) • 17.8 kB
JavaScript
import { expect, use } from 'chai';
import chaiAsPromised from 'chai-as-promised';
import { AbstractNode } from '../src/lang/abstracts/AbstractNode.js';
import { registry } from '../src/lang/registry.js';
import '../src/lang/index.js';
use(chaiAsPromised);
class DummyNode1 extends AbstractNode {
static get syntaxRules() {
return [
{ as: 'prop1', type: 'identifier' },
{ as: 'prop2', type: 'number_literal', optional: true },
];
}
}
registry.DummyNode1 = DummyNode1;
class DummyNode2 extends AbstractNode {
static get syntaxRules() {
return [
{
syntax: [
{ as: 'field1', type: 'identifier' },
{ as: 'condition', type: 'operator', value: ['IS', 'IS NOT'] },
{ as: 'field2', type: 'null_literal', value: 'NULL' },
]
},
{
dialect: 'postgres',
syntax: [
{ as: 'source', type: 'identifier' },
{ as: 'alias', type: 'identifier', optional: true },
{ as: 'cols', type: 'brace_block' }
],
}
];
}
}
registry.DummyNode2 = DummyNode2;
class DummyNode3 extends AbstractNode {
static get syntaxRules() {
return [
{ as: 'name', type: 'identifier' },
{ as: 'value', type: 'string_literal', optional: true },
{ as: 'operator', type: 'operator', value: ['+', '-'], optional: true }, // Make optional
{
optional: true, // Make this whole block optional
syntax: [
{ as: 'left', type: 'identifier' },
{ as: 'op', type: 'operator', value: '=', optional: true },
{
syntaxes: [
{ as: 'right', type: 'number_literal', value: 'eee' },
{ as: 'right', type: 'number_literal' },
{ as: 'right', type: 'number_literal', optional: true },
]
},
]
},
{
dialect: 'mysql',
syntax: [
{ as: 'items1', type: 'DummyNode3', arity: Infinity, optional: true, },
{ as: 'items2', type: 'DummyNode3', arity: { min: 2, max: 4 }, optional: true, },
],
}
];
}
}
registry.DummyNode3 = DummyNode3;
const jsonfySchemaSet = (schemaSet) => [...schemaSet].map((sch) => Object.fromEntries(sch));
describe('AbstractNode schema compilation, hydration & jsonfication - DummyNode2 (level 1 complexity)', () => {
it('should compile correct schema', () => {
const schemaSet = DummyNode1.compileASTSchemaFromSyntaxRules();
const expected = [{
prop1: { rulePath: 'DUMMY_NODE1.0<prop1>', type: 'identifier' },
prop2: {
rulePath: 'DUMMY_NODE1.1<prop2>',
type: 'number_literal',
optional: true
}
}];
expect(jsonfySchemaSet(schemaSet)).to.deep.eq(expected);
});
it('should hydrate and jsonfy fully populated JSON', () => {
const json = { prop1: 'id', prop2: '42' };
const node = DummyNode1.fromJSON(json);
expect(node.jsonfy()).to.deep.eq({
nodeName: 'DUMMY_NODE1',
...json
});
});
it('should hydrate and jsonfy JSON with omitted optional fields', () => {
const json = { prop1: 'id' };
const node = DummyNode1.fromJSON(json);
expect(node.jsonfy()).to.deep.eq({
nodeName: 'DUMMY_NODE1',
prop1: 'id',
});
});
});
describe('AbstractNode schema compilation, hydration & jsonfication - DummyNode2 (level 2 complexity)', () => {
describe('with dialect: postgres', () => {
it('should compile schema for postgres dialect', () => {
const schemaSet = DummyNode2.compileASTSchemaFromSyntaxRules({ dialect: 'postgres' });
const expected = [{
field1: { rulePath: 'DUMMY_NODE2.0.syntax.0<field1>', type: 'identifier' },
condition: {
rulePath: 'DUMMY_NODE2.0.syntax.1<condition>',
type: 'operator',
value: ['IS', 'IS NOT']
},
field2: {
rulePath: 'DUMMY_NODE2.0.syntax.2<field2>',
type: 'null_literal',
value: 'NULL'
},
source: { rulePath: 'DUMMY_NODE2.1.syntax.0<source>', type: 'identifier' },
alias: {
rulePath: 'DUMMY_NODE2.1.syntax.1<alias>',
type: 'identifier',
optional: true
},
cols: { rulePath: 'DUMMY_NODE2.1.syntax.2<cols>', type: 'brace_block' }
}];
expect(jsonfySchemaSet(schemaSet)).to.deep.eq(expected);
});
it('should hydrate and jsonfy alt-syntax with all fields', () => {
const json = {
field1: 'value1',
condition: 'IS',
field2: 'NULL',
source: 'users',
alias: 'u',
cols: []
};
const node = DummyNode2.fromJSON(json, { dialect: 'postgres' });
expect(node.jsonfy()).to.deep.eq({
nodeName: 'DUMMY_NODE2',
...json
});
});
it('should hydrate and jsonfy alt-syntax with omitted optional alias', () => {
const json = {
field1: 'value1',
condition: 'IS NOT',
field2: 'NULL',
source: 'users',
cols: []
};
const node = DummyNode2.fromJSON(json, { dialect: 'postgres' });
expect(node.jsonfy()).to.deep.eq({
nodeName: 'DUMMY_NODE2',
...json,
});
});
});
describe('with dialect: mysql', () => {
it('should compile schema for mysql dialect', () => {
const schemaSet = DummyNode2.compileASTSchemaFromSyntaxRules({ dialect: 'mysql' });
const expected = [{
field1: { rulePath: 'DUMMY_NODE2.0.syntax.0<field1>', type: 'identifier' },
condition: {
rulePath: 'DUMMY_NODE2.0.syntax.1<condition>',
type: 'operator',
value: ['IS', 'IS NOT']
},
field2: {
rulePath: 'DUMMY_NODE2.0.syntax.2<field2>',
type: 'null_literal',
value: 'NULL'
}
}];
expect(jsonfySchemaSet(schemaSet)).to.deep.eq(expected);
});
it('should hydrate and jsonfy alt-syntax with all fields', () => {
const json = {
field1: 'value1',
condition: 'IS',
field2: 'NULL'
};
const node = DummyNode2.fromJSON(json, { dialect: 'mysql' });
expect(node.jsonfy()).to.deep.eq({
nodeName: 'DUMMY_NODE2',
...json
});
});
it('should hydrate and jsonfy alt-syntax with omitted optional alias', () => {
const json = {
field1: 'value1',
condition: 'IS NOT',
field2: 'NULL'
};
const node = DummyNode2.fromJSON(json, { dialect: 'mysql' });
expect(node.jsonfy()).to.deep.eq({
nodeName: 'DUMMY_NODE2',
...json
});
});
});
});
describe('AbstractNode schema compilation, hydration & jsonfication - DummyNode2 (level 3 complexity)', () => {
describe('with dialect: postgres', () => {
it('should compile a complex AST schema from simple syntax rules, "ignoring" additional MySQL-specific rules', () => {
const schemaSet = DummyNode3.compileASTSchemaFromSyntaxRules({ dialect: 'postgres' });
expect(schemaSet).to.be.instanceOf(Set).with.lengthOf(2);
const [schema1, schema2] = jsonfySchemaSet(schemaSet);
expect(schema1).to.not.haveOwnProperty('items1');
expect(schema2).to.not.haveOwnProperty('items1');
expect(schema1.right).to.include({ value: 'eee' });
expect(schema2.right).to.not.haveOwnProperty('value');
expect(schema1).to.deep.eq({
name: { rulePath: 'DUMMY_NODE3.0<name>', type: 'identifier' },
value: {
rulePath: 'DUMMY_NODE3.1<value>',
type: 'string_literal',
optional: true
},
operator: {
rulePath: 'DUMMY_NODE3.2<operator>',
type: 'operator',
value: ['+', '-'],
optional: true
},
left: {
rulePath: 'DUMMY_NODE3.3.syntax.0<left>',
type: 'identifier',
optional: true
},
op: {
rulePath: 'DUMMY_NODE3.3.syntax.1<op>',
type: 'operator',
value: '=',
optional: true
},
right: {
rulePath: 'DUMMY_NODE3.3.syntax.2.syntaxes.0.<right>',
type: 'number_literal',
value: 'eee',
optional: true,
dependencies: ['left']
}
});
expect(schema2).to.deep.eq({
name: { rulePath: 'DUMMY_NODE3.0<name>', type: 'identifier' },
value: {
rulePath: 'DUMMY_NODE3.1<value>',
type: 'string_literal',
optional: true
},
operator: {
rulePath: 'DUMMY_NODE3.2<operator>',
type: 'operator',
value: ['+', '-'],
optional: true,
},
left: {
rulePath: 'DUMMY_NODE3.3.syntax.0<left>',
type: 'identifier',
optional: true
},
op: {
rulePath: 'DUMMY_NODE3.3.syntax.1<op>',
type: 'operator',
value: '=',
optional: true
},
right: {
rulePath: 'DUMMY_NODE3.3.syntax.2.syntaxes.1.<right>',
type: 'number_literal',
optional: true,
dependencies: ['left']
}
});
});
it('should hydrate and jsonfy full input with all "optionals" provided', () => {
const json = {
name: 'a',
value: 'hello',
operator: '+',
left: 'b',
op: '=',
right: '123'
};
const node = DummyNode3.fromJSON(json, { dialect: 'postgres' });
expect(node.jsonfy()).to.deep.eq({
nodeName: 'DUMMY_NODE3',
...json
});
});
it('should hydrate and jsonfy minimal valid input (ommitting "optionals")', () => {
const json = {
name: 'a',
operator: '+',
};
const node = DummyNode3.fromJSON(json, { dialect: 'postgres' });
expect(node.jsonfy()).to.deep.eq({
nodeName: 'DUMMY_NODE3',
name: 'a',
operator: '+',
});
});
it('should FAIL to hydrate and jsonfy if "requireds" are ommitted, or "unknowns" provided', () => {
const jsons = [{ operator: '+' }, { name: 'a', extra: 'foo' }];
for (const json of jsons) {
const node = DummyNode3.fromJSON(json, { dialect: 'postgres' });
expect(node).to.be.undefined;
}
for (const json of jsons) {
const node = () => DummyNode3.fromJSON({ nodeName: 'DUMMY_NODE3', ...json }, { dialect: 'postgres' });
expect(node).to.throw;
}
});
});
describe('with dialect: mysql', () => {
it('should compile a complex AST schema from simple syntax rules, "honouring" additional MySQL-specific rules', () => {
const schemaSet = DummyNode3.compileASTSchemaFromSyntaxRules({ dialect: 'mysql' });
expect(schemaSet).to.be.instanceOf(Set).with.lengthOf(2);
const [schema1, schema2] = jsonfySchemaSet(schemaSet);
expect(schema1.right).to.include({ value: 'eee' });
expect(schema2.right).to.not.haveOwnProperty('value');
expect(schema1).to.deep.eq({
name: { rulePath: 'DUMMY_NODE3.0<name>', type: 'identifier' },
value: {
rulePath: 'DUMMY_NODE3.1<value>',
type: 'string_literal',
optional: true
},
operator: {
rulePath: 'DUMMY_NODE3.2<operator>',
type: 'operator',
value: ['+', '-'],
optional: true,
},
left: {
rulePath: 'DUMMY_NODE3.3.syntax.0<left>',
type: 'identifier',
optional: true
},
op: {
rulePath: 'DUMMY_NODE3.3.syntax.1<op>',
type: 'operator',
value: '=',
optional: true
},
right: {
rulePath: 'DUMMY_NODE3.3.syntax.2.syntaxes.0.<right>',
type: 'number_literal',
value: 'eee',
optional: true,
dependencies: ['left']
},
items1: {
rulePath: 'DUMMY_NODE3.4.syntax.0<items1>',
type: 'DummyNode3',
arity: Infinity,
optional: true,
},
items2: {
rulePath: 'DUMMY_NODE3.4.syntax.1<items2>',
type: 'DummyNode3',
arity: { min: 2, max: 4 },
optional: true,
}
});
expect(schema2).to.deep.eq({
name: { rulePath: 'DUMMY_NODE3.0<name>', type: 'identifier' },
value: {
rulePath: 'DUMMY_NODE3.1<value>',
type: 'string_literal',
optional: true
},
operator: {
rulePath: 'DUMMY_NODE3.2<operator>',
type: 'operator',
value: ['+', '-'],
optional: true,
},
left: {
rulePath: 'DUMMY_NODE3.3.syntax.0<left>',
type: 'identifier',
optional: true
},
op: {
rulePath: 'DUMMY_NODE3.3.syntax.1<op>',
type: 'operator',
value: '=',
optional: true
},
right: {
rulePath: 'DUMMY_NODE3.3.syntax.2.syntaxes.1.<right>',
type: 'number_literal',
optional: true,
dependencies: ['left']
},
items1: {
rulePath: 'DUMMY_NODE3.4.syntax.0<items1>',
type: 'DummyNode3',
arity: Infinity,
optional: true,
},
items2: {
rulePath: 'DUMMY_NODE3.4.syntax.1<items2>',
type: 'DummyNode3',
arity: { min: 2, max: 4 },
optional: true,
}
});
});
it('should include "items1" only for mysql dialect', () => {
const mysqlJson = {
name: 'a',
operator: '-',
items1: [
{
name: 'b',
operator: '+',
}
],
items2: [
{
name: 'b',
operator: '+'
},
{
name: 'b',
operator: '+'
}
]
};
const node = DummyNode3.fromJSON(mysqlJson, { dialect: 'mysql' });
const output = node.jsonfy();
expect(output.nodeName).to.equal('DUMMY_NODE3');
expect(output.items1).to.have.lengthOf(1);
expect(output.items2).to.have.lengthOf(2);
});
it('should FAIL to hydrate and jsonfy if "requireds" are ommitted, or "arity" constraints failed', () => {
const jsons = [{ operator: '+' }, { name: 'a', items2: [] }];
for (const json of jsons) {
const node = DummyNode3.fromJSON(json, { dialect: 'mysql' });
expect(node).to.be.undefined;
}
for (const json of jsons) {
const node = () => DummyNode3.fromJSON({ nodeName: 'DUMMY_NODE3', ...json }, { dialect: 'mysql' });
expect(node).to.throw;
}
});
});
});