@latitude-data/sql-compiler
Advanced tools
Comparing version 1.0.0 to 1.1.0
# @latitude-data/sql-compiler | ||
## 1.1.0 | ||
### Minor Changes | ||
- d34d824: Added support for spread syntax | ||
## 1.0.0 | ||
@@ -4,0 +10,0 @@ |
import { QueryMetadata } from './types'; | ||
export declare function mergeMetadata(...metadata: QueryMetadata[]): QueryMetadata; | ||
export declare function emptyMetadata(): QueryMetadata; | ||
export declare function isIterable(obj: unknown): obj is Iterable<unknown>; | ||
export declare function hasContent(iterable: Iterable<unknown>): Promise<boolean>; | ||
//# sourceMappingURL=utils.d.ts.map |
@@ -106,2 +106,10 @@ declare const _default: { | ||
}; | ||
invalidSpreadInObject: (property: string) => { | ||
code: string; | ||
message: string; | ||
}; | ||
invalidSpreadInArray: (element: string) => { | ||
code: string; | ||
message: string; | ||
}; | ||
unsupportedOperator: (operator: string) => { | ||
@@ -108,0 +116,0 @@ code: string; |
{ | ||
"name": "@latitude-data/sql-compiler", | ||
"version": "1.0.0", | ||
"version": "1.1.0", | ||
"license": "LGPL", | ||
@@ -5,0 +5,0 @@ "description": "Compiler for Latitude's custom sql sintax based on svelte", |
@@ -377,15 +377,43 @@ import { compile, emptyMetadata } from '..' | ||
describe('each loops', async () => { | ||
it('prints each content for each element in the array', async () => { | ||
const sql = "{#each ['a', 'b', 'c'] as element} {element} {/each}" | ||
const result = await compile({ | ||
query: sql, | ||
it('iterates over any iterable object', async () => { | ||
const sql1 = '{#each [1, 2, 3] as element} {element} {/each}' | ||
const sql2 = '{#each "foo" as element} {element} {/each}' | ||
const result1 = await compile({ | ||
query: sql1, | ||
resolveFn, | ||
supportedMethods: {}, | ||
}) | ||
const result2 = await compile({ | ||
query: sql2, | ||
resolveFn, | ||
supportedMethods: {}, | ||
}) | ||
expect(result).toBe('$[[a]]$[[b]]$[[c]]') | ||
expect(result1).toBe('$[[1]]$[[2]]$[[3]]') | ||
expect(result2).toBe('$[[f]]$[[o]]$[[o]]') | ||
}) | ||
it('gives access to the index of the element', async () => { | ||
const sql = "{#each ['a', 'b', 'c'] as element, index} {index} {/each}" | ||
it('computes the else block when the element is not iterable', async () => { | ||
const sql1 = '{#each 5 as element} {element} {:else} FOO {/each}' | ||
const sql2 = | ||
'{#each { a: 1, b: 2, c: 3 } as element} {element} {:else} FOO {/each}' | ||
const result1 = await compile({ | ||
query: sql1, | ||
resolveFn, | ||
supportedMethods: {}, | ||
}) | ||
const result2 = await compile({ | ||
query: sql2, | ||
resolveFn, | ||
supportedMethods: {}, | ||
}) | ||
expect(result1).toBe('FOO') | ||
expect(result2).toBe('FOO') | ||
}) | ||
it('computes the else block when the iterable object is empty', async () => { | ||
const sql = '{#each [] as element} {element} {:else} var {/each}' | ||
const result = await compile({ | ||
@@ -397,7 +425,7 @@ query: sql, | ||
expect(result).toBe('$[[0]]$[[1]]$[[2]]') | ||
expect(result).toBe('var') | ||
}) | ||
it('replaces a variable with the value of the element', async () => { | ||
const sql = "{#each ['a', 'b', 'c'] as element} {element} {/each}" | ||
it('does not do anything when the iterable object is not iterable and there is no else block', async () => { | ||
const sql = '{#each 5 as element} {element} {/each}' | ||
const result = await compile({ | ||
@@ -409,7 +437,7 @@ query: sql, | ||
expect(result).toBe('$[[a]]$[[b]]$[[c]]') | ||
expect(result).toBe('') | ||
}) | ||
it('prints else content when the array is empty', async () => { | ||
const sql = '{#each [] as element} {element} {:else} var {/each}' | ||
it('gives access to the index of the element', async () => { | ||
const sql = "{#each ['a', 'b', 'c'] as element, index} {index} {/each}" | ||
const result = await compile({ | ||
@@ -421,7 +449,7 @@ query: sql, | ||
expect(result).toBe('var') | ||
expect(result).toBe('$[[0]]$[[1]]$[[2]]') | ||
}) | ||
it('prints else content when the element is not an array', async () => { | ||
const sql = '{#each 5 as element} {element} {:else} var {/each}' | ||
it('replaces a variable with the value of the element', async () => { | ||
const sql = "{#each ['a', 'b', 'c'] as element} {element} {/each}" | ||
const result = await compile({ | ||
@@ -433,3 +461,3 @@ query: sql, | ||
expect(result).toBe('var') | ||
expect(result).toBe('$[[a]]$[[b]]$[[c]]') | ||
}) | ||
@@ -436,0 +464,0 @@ |
@@ -10,3 +10,3 @@ import { BaseNode, type TemplateNode } from '../parser/interfaces' | ||
import { getLogicNodeMetadata, resolveLogicNode } from './logic' | ||
import { emptyMetadata, mergeMetadata } from './utils' | ||
import { emptyMetadata, hasContent, isIterable, mergeMetadata } from './utils' | ||
import { createHash } from 'node:crypto' | ||
@@ -194,3 +194,3 @@ | ||
if (baseNode.type === 'EachBlock') { | ||
const iterableElement = await resolveLogicNode({ | ||
const iterableElement = (await resolveLogicNode({ | ||
node: baseNode.expression, | ||
@@ -202,4 +202,8 @@ scope: localScope, | ||
resolveFn: this.context.resolveFn, | ||
}) | ||
if (!Array.isArray(iterableElement) || !iterableElement.length) { | ||
})) as Iterable<unknown> | ||
if ( | ||
!isIterable(iterableElement) || | ||
!(await hasContent(iterableElement)) | ||
) { | ||
return await this.resolveBaseNode(baseNode.else, localScope, depth + 1) | ||
@@ -218,4 +222,4 @@ } | ||
const parsedChildren: string[] = [] | ||
for (let i = 0; i < iterableElement.length; i++) { | ||
const element = iterableElement[i] | ||
let i = 0 | ||
for await (const element of iterableElement) { | ||
if (indexVar) localScope.set(indexVar, i) | ||
@@ -230,2 +234,3 @@ localScope.set(contextVar, element) | ||
) | ||
i++ | ||
} | ||
@@ -232,0 +237,0 @@ return parsedChildren.join('') || '' |
import { getLogicNodeMetadata, resolveLogicNode } from '..' | ||
import { emptyMetadata, mergeMetadata } from '../../utils' | ||
import errors from '../../../error/errors' | ||
import { emptyMetadata, isIterable, mergeMetadata } from '../../utils' | ||
import type { ReadNodeMetadataProps, ResolveNodeProps } from '../types' | ||
@@ -15,12 +16,30 @@ import type { ArrayExpression } from 'estree' | ||
}: ResolveNodeProps<ArrayExpression>) { | ||
return await Promise.all( | ||
node.elements.map((element) => | ||
element | ||
? resolveLogicNode({ | ||
node: element, | ||
...props, | ||
}) | ||
: null, | ||
), | ||
) | ||
const { raiseError } = props | ||
const resolvedArray = [] | ||
for (const element of node.elements) { | ||
if (!element) continue | ||
if (element.type !== 'SpreadElement') { | ||
const value = await resolveLogicNode({ | ||
node: element, | ||
...props, | ||
}) | ||
resolvedArray.push(value) | ||
continue | ||
} | ||
const spreadObject = await resolveLogicNode({ | ||
node: element.argument, | ||
...props, | ||
}) | ||
if (!isIterable(spreadObject)) { | ||
raiseError(errors.invalidSpreadInArray(typeof spreadObject), element) | ||
} | ||
for await (const value of spreadObject as Iterable<unknown>) { | ||
resolvedArray.push(value) | ||
} | ||
} | ||
return resolvedArray | ||
} | ||
@@ -34,5 +53,7 @@ | ||
node.elements.map(async (element) => { | ||
if (element) | ||
return await getLogicNodeMetadata({ node: element, ...props }) | ||
return emptyMetadata() | ||
if (!element) return emptyMetadata() | ||
if (element.type === 'SpreadElement') { | ||
return await getLogicNodeMetadata({ node: element.argument, ...props }) | ||
} | ||
return await getLogicNodeMetadata({ node: element, ...props }) | ||
}), | ||
@@ -39,0 +60,0 @@ ) |
@@ -5,8 +5,3 @@ import { getLogicNodeMetadata, resolveLogicNode } from '..' | ||
import { ReadNodeMetadataProps, type ResolveNodeProps } from '../types' | ||
import { | ||
Property, | ||
SpreadElement, | ||
type Identifier, | ||
type ObjectExpression, | ||
} from 'estree' | ||
import { type Identifier, type ObjectExpression } from 'estree' | ||
@@ -25,13 +20,29 @@ /** | ||
for (const prop of node.properties) { | ||
if (prop.type !== 'Property') { | ||
throw raiseError(errors.invalidObjectKey, node) | ||
if (prop.type === 'SpreadElement') { | ||
const spreadObject = await resolveLogicNode({ | ||
node: prop.argument, | ||
scope, | ||
raiseError, | ||
...props, | ||
}) | ||
if (typeof spreadObject !== 'object') { | ||
raiseError(errors.invalidSpreadInObject(typeof spreadObject), prop) | ||
} | ||
Object.entries(spreadObject as object).forEach(([key, value]) => { | ||
resolvedObject[key] = value | ||
}) | ||
continue | ||
} | ||
const key = prop.key as Identifier | ||
const value = await resolveLogicNode({ | ||
node: prop.value, | ||
scope, | ||
raiseError, | ||
...props, | ||
}) | ||
resolvedObject[key.name] = value | ||
if (prop.type === 'Property') { | ||
const key = prop.key as Identifier | ||
const value = await resolveLogicNode({ | ||
node: prop.value, | ||
scope, | ||
raiseError, | ||
...props, | ||
}) | ||
resolvedObject[key.name] = value | ||
continue | ||
} | ||
throw raiseError(errors.invalidObjectKey, prop) | ||
} | ||
@@ -41,6 +52,2 @@ return resolvedObject | ||
function isProperty(prop: Property | SpreadElement): prop is Property { | ||
return prop.type === 'Property' | ||
} | ||
export async function readMetadata({ | ||
@@ -51,14 +58,24 @@ node, | ||
const propertiesMetadata = await Promise.all( | ||
node.properties.filter(isProperty).map((prop) => | ||
Promise.all([ | ||
getLogicNodeMetadata({ | ||
node: prop.key, | ||
...props, | ||
}), | ||
getLogicNodeMetadata({ | ||
node: prop.value, | ||
...props, | ||
}), | ||
]), | ||
), | ||
node.properties | ||
.map((prop) => { | ||
if (prop.type === 'SpreadElement') { | ||
return getLogicNodeMetadata({ | ||
node: prop.argument, | ||
...props, | ||
}) | ||
} | ||
if (prop.type === 'Property') { | ||
return Promise.all([ | ||
getLogicNodeMetadata({ | ||
node: prop.key, | ||
...props, | ||
}), | ||
getLogicNodeMetadata({ | ||
node: prop.value, | ||
...props, | ||
}), | ||
]) | ||
} | ||
}) | ||
.filter((p) => p !== undefined), | ||
) | ||
@@ -65,0 +82,0 @@ |
@@ -43,1 +43,12 @@ import { QueryMetadata } from './types' | ||
} | ||
export function isIterable(obj: unknown): obj is Iterable<unknown> { | ||
return (obj as Iterable<unknown>)?.[Symbol.iterator] !== undefined | ||
} | ||
export async function hasContent(iterable: Iterable<unknown>) { | ||
for await (const _ of iterable) { | ||
return true | ||
} | ||
return false | ||
} |
@@ -115,2 +115,10 @@ function getKlassName(error: unknown): string { | ||
}, | ||
invalidSpreadInObject: (property: string) => ({ | ||
code: 'invalid-spread-in-object', | ||
message: `Property '${property}' is not valid for spreading`, | ||
}), | ||
invalidSpreadInArray: (element: string) => ({ | ||
code: 'invalid-spread-in-array', | ||
message: `Element '${element}' is not iterable`, | ||
}), | ||
unsupportedOperator: (operator: string) => ({ | ||
@@ -143,3 +151,3 @@ code: 'unsupported-operator', | ||
code: 'not-a-function', | ||
message: `Object '${objectType}' is callable`, | ||
message: `Object '${objectType}' is not callable`, | ||
}), | ||
@@ -146,0 +154,0 @@ functionCallError: (err: unknown) => { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
385645
6493